/*******************************************************************************
* CogTool Copyright Notice and Distribution Terms
* CogTool 1.3, Copyright (c) 2005-2013 Carnegie Mellon University
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* CogTool is free software; you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* CogTool is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with CogTool; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
* CogTool makes use of several third-party components, with the
* following notices:
*
* Eclipse SWT version 3.448
* Eclipse GEF Draw2D version 3.2.1
*
* Unless otherwise indicated, all Content made available by the Eclipse
* Foundation is provided to you under the terms and conditions of the Eclipse
* Public License Version 1.0 ("EPL"). A copy of the EPL is provided with this
* Content and is also available at http://www.eclipse.org/legal/epl-v10.html.
*
* CLISP version 2.38
*
* Copyright (c) Sam Steingold, Bruno Haible 2001-2006
* This software is distributed under the terms of the FSF Gnu Public License.
* See COPYRIGHT file in clisp installation folder for more information.
*
* ACT-R 6.0
*
* Copyright (c) 1998-2007 Dan Bothell, Mike Byrne, Christian Lebiere &
* John R Anderson.
* This software is distributed under the terms of the FSF Lesser
* Gnu Public License (see LGPL.txt).
*
* Apache Jakarta Commons-Lang 2.1
*
* This product contains software developed by the Apache Software Foundation
* (http://www.apache.org/)
*
* jopt-simple version 1.0
*
* Copyright (c) 2004-2013 Paul R. Holser, Jr.
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Mozilla XULRunner 1.9.0.5
*
* The contents of this file are subject to the Mozilla Public License
* Version 1.1 (the "License"); you may not use this file except in
* compliance with the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/.
* Software distributed under the License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
* License for the specific language governing rights and limitations
* under the License.
*
* The J2SE(TM) Java Runtime Environment version 5.0
*
* Copyright 2009 Sun Microsystems, Inc., 4150
* Network Circle, Santa Clara, California 95054, U.S.A. All
* rights reserved. U.S.
* See the LICENSE file in the jre folder for more information.
******************************************************************************/
package edu.cmu.cs.hcii.cogtool.model;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import edu.cmu.cs.hcii.cogtool.CogToolPref;
// The heavy lifting is delegated to instances of the static inner class
// StepParser. Each such instances corresponds to a particular kind of step,
// or possible set of steps that occur together, and are bounded by a starting
// end ending trace line, possibly with intermediate trace lines.
// A StepParser always acts on lines of a single module, and has regular
// expressions matching the appropriate part of the trace line after the
// module name. Methods in the TraceParser construct appropriate ResultSteps;
// the surrounding code that calls these methods takes care of shared
// operations such as setting the time and duration of the ResultStep.
//
// The trace lines are matched in order against the possible StepParsers.
// Only "normal" trace lines, consisting of a time, module name, and other
// information are matched, other lines in the trace being skipped.
// Once a StepParser has matched the beginning trace line it stores information
// in a StepParser.State object that can be consulted when further lines
// denoting the end and/or intermediate trace lines are matched.
//
// The various StepParsers are all declared in a static initializer at the
// end of this file. Adding further trace information typically consists
// of declaring a new StepParser there.
//
// In a few cases there is more global information stored in the
// surrounding ACTRTraceParser, in addition to the State object corresponding
// to a single ResultStep.
public class ACTRTraceParser implements TraceParser<ResultStep>
{
// ACT-R in its infinite wisdom does not emit a trace line for the start of
// motor operation initialization, only a line for its end. So we need to
// work backwards. Fortunately init time in ACT-R is constant. If we change
// it in ACT-R with spp. though, we'll need to update the value here, too.
protected static final int INIT_TIME = 50; // msec
protected static final String DEFAULT_HEARD_TEXT = "<text heard>";
protected static final Pattern TRACE_LINE_PAT =
Pattern.compile("\\s+(\\d+\\.\\d\\d\\d)\\s+(\\w+)\\s+(.*?)\\s*");
protected static final Map<String, List<StepParser>> stepParsers =
new HashMap<String, List<StepParser>>();
protected static final Map<String, List<StepParser.State>> pendingSteps =
new HashMap<String, List<StepParser.State>>();
protected static boolean buildingStepParsers = true;
protected final List<ResultStep> resultSteps = new ArrayList<ResultStep>();
protected int line = -1;
protected double time = 0.0;
protected ResultStep lastProductionResultStep = null;
/**
* Parses an ACT-R 6 trace and returns a list of ResultStep objects
* that represent that trace.
*
* @param traceLines
* @return List of ResultStep objects
*/
public List<ResultStep> parseTrace(List<String> traceLines)
{
if (buildingStepParsers) {
throw new IllegalStateException(
"Data required for parsing ACT-R trace not properly initialized");
}
for (List<StepParser.State> lst : pendingSteps.values())
{
lst.clear();
}
for (String s: traceLines)
{
++line;
Matcher m = TRACE_LINE_PAT.matcher(s);
if (! m.matches()) {
if (s.startsWith("#|Warning: Move-cursor action aborted because cursor is at requested target")) {
abandonPendingMoveCuror();
}
continue;
}
try {
time = (Double.parseDouble(m.group(1)) * 1000);
} catch (NumberFormatException e) {
throw new IllegalStateException(
"Text matching pattern specifying double not parseable as double " +
m.group(1));
}
if (time < 0.0) {
continue;
}
String module = m.group(2);
String detail = m.group(3);
processTraceLine(module, detail);
}
List<StepParser.State> finishThis = pendingSteps.get("COGTOOL");
if (! finishThis.isEmpty()) {
StepParser.State s = finishThis.get(0);
s.intermediate = false;
processTraceLine("COGTOOL", "Finished");
}
abandonPendingEyeMovementPreps(null);
for (String k : pendingSteps.keySet())
{
List<StepParser.State> lst = pendingSteps.get(k);
if (! lst.isEmpty()) {
// TODO figure out how to deal with this more gracefully;
// for now just write something to stderr, and ignore it
// throw new IllegalStateException(
// "Not all ResultSteps were completed: " + lst.get(0));
System.err.println("Not all ResultSteps were completed: " +
k + " -> " + lst.get(0));
}
}
return resultSteps;
}
protected static void abandonPendingEyeMovementPreps(StepParser.State leaveThisOne) {
List<StepParser.State> lst = pendingSteps.get("VISION");
for (Iterator<StepParser.State> it = lst.iterator(); it.hasNext(); ) {
StepParser.State other = it.next();
if (other != leaveThisOne && other.data == null) {
it.remove();
}
}
}
// This gets fired when there is an attempt to move the cursor to exactly
// where it is already. In KLM CogTool this should never happen, but in CT-E
// if you click the back button two or more times in succession it does.
protected static void abandonPendingMoveCuror() {
List<StepParser.State> lst = pendingSteps.get("MOTOR");
for (Iterator<StepParser.State> it = lst.iterator(); it.hasNext(); ) {
Object d = it.next().data;
if (d instanceof String &&
((String)d).startsWith("Move Cursor")) {
it.remove();
return;
}
}
}
protected void processTraceLine(String module, String detail)
{
// First see if this trace line ends a pending step
List<StepParser.State> pending = pendingSteps.get(module);
if (pending != null && ! pending.isEmpty()) {
for (Iterator<StepParser.State> it = pending.iterator(); it.hasNext(); ) {
StepParser.State state = it.next();
Matcher m = state.pattern().matcher(detail);
if (m.lookingAt()) {
// grab the following value before execute has a chance to change it
boolean intermed = state.intermediate;
ResultStep result = state.execute(this, m);
if (result != null) {
if (result.startTime < 0.0); {
result.startTime = state.startTime;
}
if (result.duration < 1.0) {
result.duration = (time - state.startTime);
}
if (result.traceStart < 0) {
result.traceStart = state.startIndex;
}
// TODO investigate how the consumer of this information
// is fiddling it -- I suspect it's doing a subtract
// one that it really shouldn't be, and that's why
// we're having to add one here!
result.traceEnd = (line + 1);
resultSteps.add(result);
if (intermed) {
state.startTime = time;
state.startIndex = line;
}
else {
it.remove();
}
return;
}
}
}
}
// If not does it start a new one?
List<StepParser> parsers = stepParsers.get(module);
if (parsers != null) {
for (StepParser p : parsers) {
Matcher m = p.startPat.matcher(detail);
if (m.lookingAt()) {
StepParser.State state = p.new State(time, line);
state.lastProduction = lastProductionResultStep;
p.startAction(this, m, state);
if (p.intermediatePat != null) {
state.intermediate = true;
}
pendingSteps.get(module).add(state);
return;
}
}
}
}
protected static abstract class StepParser
{
final String module;
final Pattern startPat;
final Pattern intermediatePat;
final Pattern endPat;
class State
{
double startTime;
int startIndex;
Object data = null;
boolean intermediate = false;
ResultStep.ResultStepDependency dependency = null;
ResultStep lastProduction = null;
State(double t, int i)
{
startTime = t;
startIndex = i;
}
@Override
public String toString()
{
return ("State:"
+ module + ":"
+ startPat.pattern() + ":"
+ startIndex + ":"
+ data);
}
Pattern pattern()
{
return (intermediate ? intermediatePat : endPat);
}
ResultStep execute(ACTRTraceParser parser, Matcher match)
{
// grab the dependency before executing the action, since
// the action may modify it
ResultStep.ResultStepDependency d = dependency;
ResultStep result = null;
if (intermediate) {
intermediate = false;
result = intermediateAction(parser, match, this);
if (result == null) {
intermediate = true;
}
} else {
result = endAction(parser, match, this);
}
if (result != null && d != null) {
result.dependencies.add(d);
}
return result;
}
protected void addProductionDependency(ResultStep rs)
{
if (lastProduction != null) {
rs.dependencies.add(new ResultStep.ResultStepDependency(lastProduction));
}
}
}
StepParser(String mod,
String start,
String imed,
String end)
{
module = mod;
startPat = Pattern.compile(start);
intermediatePat = (imed != null ? Pattern.compile(imed) : null);
endPat = Pattern.compile(end);
}
StepParser(String mod, String start, String end)
{
this(mod, start, null, end);
}
void startAction(ACTRTraceParser parser,
Matcher match,
State state)
{
// Will override if any extra state needs to be preserved
}
ResultStep intermediateAction(ACTRTraceParser parser,
Matcher match,
State state) {
// Will normally override whenever intermediate pattern is supplied
return null;
}
abstract ResultStep endAction(ACTRTraceParser parser,
Matcher match,
State state);
}
protected static void declareStep(StepParser p)
{
if (! buildingStepParsers) {
throw new IllegalStateException("Can no longer add StepParsers");
}
List<StepParser> lst = stepParsers.get(p.module);
if (lst == null) {
lst = new ArrayList<StepParser>();
stepParsers.put(p.module, lst);
}
lst.add(p);
}
protected static void finishBuildingStepParsers() {
buildingStepParsers = false;
for (String mod : stepParsers.keySet()) {
pendingSteps.put(mod, new ArrayList<StepParser.State>());
}
}
protected LinkedList<ResultStep> pendingMotorOperations = new LinkedList<ResultStep>();
// TODO How we want motor operations displayed has evolved repeatedly,
// and has involved a variety of complications requiring knowledge
// of one step to be passed to another, but in changing ways as our
// understanding of what we want has evolved. The result is we now
// have a Byzantine structure with some undoubtedly vestigial items
// in it. It would be worth have a complete re-think of how we want to
// do this in light of what we now know is the end result we want.
static class MotorStepParser extends StepParser
{
final String label;
final boolean appendCapture;
final boolean leftHand;
MotorStepParser(String start,
String lab,
boolean appendCap,
boolean left)
{
super("MOTOR", start, "PREPARATION-COMPLETE");
label = lab;
appendCapture = appendCap;
leftHand = left;
}
MotorStepParser(String start, String lab, boolean appendCap)
{
this(start, lab, appendCap, false);
}
MotorStepParser(String start, String lab)
{
this(start, lab, false);
}
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
if (appendCapture) {
s.data = (label + m.group(1));
}
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
ResultStep result =
new ResultStep((leftHand ? ResultStep.MOTOR_LEFT_PREP_RESOURCE
: ResultStep.MOTOR_RIGHT_PREP_RESOURCE),
(appendCapture ? (String) s.data : label));
s.addProductionDependency(result);
p.pendingMotorOperations.addLast(result);
return result;
}
}
static {
// create the StepParsers
// order matters here; the earlier ones are tried before the later ones
declareStep(new StepParser("COGTOOL",
"START-SYSTEM-WAIT [0-9\\.]+ \"(.+)\"",
"SYSTEM-WAIT-DONE .*")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
ResultStep result = new ResultStep(ResultStep.SYSTEM_RESOURCE,
(String) s.data);
return result;
}
});
declareStep(new StepParser("PROCEDURAL",
"PRODUCTION-SELECTED ([^*].*)",
"PRODUCTION-FIRED (.+)")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
String lab = (String) s.data;
if (lab != null && lab.equals(m.group(1))) {
ResultStep result =
new ResultStep(ResultStep.PRODUCTIONS_RESOURCE, lab);
p.lastProductionResultStep = result;
return result;
} else {
return null;
}
}
});
declareStep(new StepParser("COGTOOL",
"TRANSITION-TO \"(.*)\"",
"TRANSITION-TO \"(.*)\"",
"Finished")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
}
protected ResultStep makeResultStep(State s)
{
return new ResultStep(ResultStep.FRAME_RESOURCE, (String) s.data);
}
@Override
ResultStep intermediateAction(ACTRTraceParser p, Matcher m, State s)
{
ResultStep result = makeResultStep(s);
s.intermediate = true;
s.data = m.group(1);
return result;
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
return makeResultStep(s);
}
});
declareStep(new StepParser("MOTOR",
"INITIATION-COMPLETE",
"FINISH-MOVEMENT")
{
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
ResultStep prep;
try {
prep = p.pendingMotorOperations.removeFirst();
} catch (NoSuchElementException e) {
// RuntimeException re =
// new IllegalStateException("Unmatched motor state");
// re.initCause(e);
// throw re;
// TODO revisit what to do in this case; for now just ignore
System.err.println("Unmatched motor state");
return null;
}
// what we visualize as "exec" time is really what ACT-R
// considers the sum prep + init + exec
s.startTime -= (prep.duration + INIT_TIME);
String resource = ResultStep.MOTOR_RIGHT_EXEC_RESOURCE;
if (prep.resource == ResultStep.MOTOR_LEFT_PREP_RESOURCE) {
resource = ResultStep.MOTOR_LEFT_EXEC_RESOURCE;
}
ResultStep result = new ResultStep(resource, prep.operation);
result.dependencies.addAll(prep.dependencies);
result.traceStart = prep.traceStart;
p.resultSteps.remove(prep);
return result;
}
});
// Several regular expressions below use the positively Luciferian
// construct
// \"((?:\\\\\"|[^\"])*)\"
// This means
// - match a quote (which needs to be escaped to get it into a Java String)
// - start a capturing group
// - then start a non-capturing group, just precedence stuff
// - then match a back slash; that's four backslashes, to get two
// into the Java String, since we need two since back slashes as an
// escape in Regex land, too
// - followed by yet another back slash quoting another quote
// - an 'or'
// - then a character class matching anything but another double quote
// (which, of course, has to be escaped with back slash for the String)
// - close the non-capturing group, and have it matching zero or more times
// - close the capturing group, to grab the whole matching thing
// - then the close double quote, appropriately escaped for the string
// In summary, it matches a Lisp string, which might have embedded
// double quotes escaped by back slashes, and captures the contents of
// that string without the surrounding quotes. Note that it does leave
// the escaping back slash with each embedded double quote in the captured
// group, however.
declareStep(new MotorStepParser("MOVE-CURSOR .* \"((?:\\\\\"|[^\"])*)\" .* NIL",
"Move Cursor to ",
true));
declareStep(new MotorStepParser("MOVE-CURSOR .* \"((?:\\\\\"|[^\"])*)\" .* T",
"Move Finger to ",
true));
declareStep(new MotorStepParser("CLICK-MOUSE NIL", "Click Mouse"));
declareStep(new MotorStepParser("CLICK-MOUSE T", "Tap"));
declareStep(new MotorStepParser("HAND-TO-HOME", "Hand to Home"));
declareStep(new MotorStepParser("HAND-TO-MOUSE", "Hand to Mouse"));
declareStep(new MotorStepParser("PRESS-MOUSE-BUTTON", "Press Mouse Button"));
declareStep(new MotorStepParser("RELEASE-MOUSE-BUTTON", "Release Mouse Button"));
declareStep(new MotorStepParser("FINGER-UP .*", "Finger Up"));
declareStep(new MotorStepParser("FINGER-DOWN .*", "Finger Down"));
declareStep(new MotorStepParser("GRASP-KNOB", "Grasp Knob"));
declareStep(new MotorStepParser("RELEASE-KNOB", "Release Knob"));
declareStep(new MotorStepParser("TURN-KNOB-90-DEG", "Turn Knob 90 Degrees"));
declareStep(new MotorStepParser("START-PRESS-KEY RIGHT (.*)",
"Press Key ",
true,
false));
declareStep(new MotorStepParser("START-PRESS-KEY LEFT (.*)",
"Press Key ",
true,
true));
declareStep(new MotorStepParser("GRAFFITI-GESTURE KEY (.*)",
"Graffiti Gesture ",
true,
false));
// TODO the following two are very Boeing application specific
// and probably don't really belong here long term.
declareStep(new MotorStepParser("REACH-FROM-LAP-TO-MCP-HDG-KNOB", "Reach from Lap to MCP HDG Knob"));
declareStep(new MotorStepParser("REACH-FROM-MCP-HDG-KNOB-TO-LAP", "Reach from MCP HDG Knob to Lap"));
declareStep(new StepParser("SPEECH",
"SPEAK TEXT (.*)",
"FINISH-MOVEMENT")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
String t = (String) s.data;
ResultStep result =
new ResultStep(ResultStep.SPEECH_EXEC_RESOURCE, t);
s.addProductionDependency(result);
return result;
}
});
declareStep(new StepParser("VISION",
"Move-attention (.*) NIL \"((?:\\\\\"|[^\"])*)\".*",
"Encoding-[cC]omplete (.*)")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = new String[] { m.group(1), m.group(2) };
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
String[] sa = (String[]) s.data;
String matchThis = m.group(1);
if (!CogToolPref.USE_EMMA.getBoolean()) {
if (matchThis.endsWith(" NIL")) {
matchThis = matchThis.substring(0, matchThis.length() - 4);
} else {
return null;
}
}
if (!matchThis.equals(sa[0])) {
return null;
}
ResultStep result =
new ResultStep(ResultStep.VISION_ENC_RESOURCE, sa[1]);
s.addProductionDependency(result);
return result;
}
});
declareStep(new StepParser("VISION",
"PREPARE-EYE-MOVEMENT",
"Preparation-complete (.*)",
"Complete-eye-movement (.*) #\\((\\d+) (\\d+)\\)")
{
@Override
void startAction(ACTRTraceParser parser,
Matcher match,
State state)
{
// If there's a pending vision prep, abandon it
abandonPendingEyeMovementPreps(state);
}
@Override
ResultStep intermediateAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
ResultStep result =
new ResultStep(ResultStep.VISION_PREP_RESOURCE,
"Eye Movement Preparation");
s.dependency =
new ResultStep.ResultStepDependency(result);
return result;
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
if (! m.group(1).equals(s.data)) {
return null;
}
ResultStep result =
new ResultStep(ResultStep.VISION_EXEC_RESOURCE,
("Eye Movement to ("
+ m.group(2)
+ ", "
+ m.group(3)
+")"));
result.dependencies.add(s.dependency);
return result;
}
});
declareStep(new StepParser("AUDIO",
"LOUDSPEAKER \"(.*)\"",
"AUDIO-ENCODING-COMPLETE .*")
{
@Override
void startAction(ACTRTraceParser p, Matcher m, State s)
{
s.data = m.group(1);
}
@Override
ResultStep endAction(ACTRTraceParser p, Matcher m, State s)
{
ResultStep result =
new ResultStep(ResultStep.HEAR_RESOURCE, (String) s.data);
s.addProductionDependency(result);
return result;
}
});
finishBuildingStepParsers();
}
}