diff options
Diffstat (limited to 'src/com/p4square/grow/model')
-rw-r--r-- | src/com/p4square/grow/model/Answer.java | 135 | ||||
-rw-r--r-- | src/com/p4square/grow/model/CircleQuestion.java | 89 | ||||
-rw-r--r-- | src/com/p4square/grow/model/ImageQuestion.java | 24 | ||||
-rw-r--r-- | src/com/p4square/grow/model/Point.java | 79 | ||||
-rw-r--r-- | src/com/p4square/grow/model/QuadQuestion.java | 89 | ||||
-rw-r--r-- | src/com/p4square/grow/model/QuadScoringEngine.java | 49 | ||||
-rw-r--r-- | src/com/p4square/grow/model/Question.java | 134 | ||||
-rw-r--r-- | src/com/p4square/grow/model/RecordedAnswer.java | 34 | ||||
-rw-r--r-- | src/com/p4square/grow/model/Score.java | 70 | ||||
-rw-r--r-- | src/com/p4square/grow/model/ScoringEngine.java | 26 | ||||
-rw-r--r-- | src/com/p4square/grow/model/SimpleScoringEngine.java | 26 | ||||
-rw-r--r-- | src/com/p4square/grow/model/SliderQuestion.java | 24 | ||||
-rw-r--r-- | src/com/p4square/grow/model/SliderScoringEngine.java | 29 | ||||
-rw-r--r-- | src/com/p4square/grow/model/TextQuestion.java | 24 |
14 files changed, 769 insertions, 63 deletions
diff --git a/src/com/p4square/grow/model/Answer.java b/src/com/p4square/grow/model/Answer.java index 4c84060..57a1e5d 100644 --- a/src/com/p4square/grow/model/Answer.java +++ b/src/com/p4square/grow/model/Answer.java @@ -4,7 +4,7 @@ package com.p4square.grow.model; -import java.util.Map; +import org.apache.log4j.Logger; /** * This is the model of an assessment question's answer. @@ -12,52 +12,131 @@ import java.util.Map; * @author Jesse Morgan <jesse@jesterpm.net> */ public class Answer { + private static final Logger LOG = Logger.getLogger(Answer.class); + + /** + * ScoreType determines how the answer will be scored. + * + */ public static enum ScoreType { - NONE, AVERAGE, TRUMP; - } - - private final String mAnswerId; - private final String mAnswerText; - private final ScoreType mType; - private final float mScoreFactor; - private final String mNextQuestionId; - - public Answer(final String id, final Map<String, Object> answer) { - mAnswerId = id; - mAnswerText = (String) answer.get("text"); - final String typeStr = (String) answer.get("type"); - if (typeStr == null) { - mType = ScoreType.AVERAGE; - } else { - mType = ScoreType.valueOf(typeStr.toUpperCase()); - } + /** + * This question has no effect on the score. + */ + NONE, - if (mType != ScoreType.NONE) { - mScoreFactor = Float.valueOf((String) answer.get("score")); - } else { - mScoreFactor = 0; - } + /** + * The score of this question is part of the average. + */ + AVERAGE, - mNextQuestionId = (String) answer.get("nextQuestion"); + /** + * The score of this question is the total score, no other questions + * matter after this point. + */ + TRUMP; + + @Override + public String toString() { + return name().toLowerCase(); + } } - public String getId() { - return mAnswerId; + private String mAnswerText; + private ScoreType mType; + private float mScoreFactor; + private String mNextQuestionId; + + public Answer() { + mType = ScoreType.AVERAGE; } + /** + * @return The text associated with the answer. + */ public String getText() { return mAnswerText; } + /** + * Set the text associated with the answer. + * @param text The new text. + */ + public void setText(String text) { + mAnswerText = text; + } + + /** + * @return the ScoreType for the Answer. + */ public ScoreType getType() { return mType; } - public float getScoreFactor() { + /** + * Set the ScoreType for the answer. + * @param type The new ScoreType. + */ + public void setType(ScoreType type) { + mType = type; + } + + /** + * @return the delta of the score if this answer is selected. + */ + public float getScore() { + if (mType == ScoreType.NONE) { + return 0; + } + return mScoreFactor; } + /** + * Set the score delta for this answer. + * @param score The new delta. + */ + public void setScore(float score) { + mScoreFactor = score; + } + + /** + * @return the id of the next question if this answer is selected, or null + * if selecting this answer has no effect. + */ public String getNextQuestion() { return mNextQuestionId; } + + /** + * Set the id of the next question when this answer is selected. + * @param id The next question id or null to proceed as usual. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; + } + + /** + * Adjust the running score for the selection of this answer. + * @param score The running score to adjust. + * @return true if scoring should continue, false if this answer trumps all. + */ + public boolean score(final Score score) { + switch (getType()) { + case TRUMP: + score.sum = getScore(); + score.count = 1; + return false; // Quit scoring. + + case AVERAGE: + LOG.error("ScoreType.AVERAGE: { delta: \"" + getScore() + "\" }"); + score.sum += getScore(); + score.count++; + break; + + case NONE: + break; + } + + return true; // Continue scoring + } } diff --git a/src/com/p4square/grow/model/CircleQuestion.java b/src/com/p4square/grow/model/CircleQuestion.java new file mode 100644 index 0000000..71acc14 --- /dev/null +++ b/src/com/p4square/grow/model/CircleQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Circle Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class CircleQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTopLeft; + private String mTopRight; + private String mBottomLeft; + private String mBottomRight; + + /** + * @return the Top Left label. + */ + public String getTopLeft() { + return mTopLeft; + } + + /** + * Set the Top Left label. + * @param s The new top left label. + */ + public void setTopLeft(String s) { + mTopLeft = s; + } + + /** + * @return the Top Right label. + */ + public String getTopRight() { + return mTopRight; + } + + /** + * Set the Top Right label. + * @param s The new top left label. + */ + public void setTopRight(String s) { + mTopRight = s; + } + + /** + * @return the Bottom Left label. + */ + public String getBottomLeft() { + return mBottomLeft; + } + + /** + * Set the Bottom Left label. + * @param s The new top left label. + */ + public void setBottomLeft(String s) { + mBottomLeft = s; + } + + /** + * @return the Bottom Right label. + */ + public String getBottomRight() { + return mBottomRight; + } + + /** + * Set the Bottom Right label. + * @param s The new top left label. + */ + public void setBottomRight(String s) { + mBottomRight = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.CIRCLE; + } +} diff --git a/src/com/p4square/grow/model/ImageQuestion.java b/src/com/p4square/grow/model/ImageQuestion.java new file mode 100644 index 0000000..d94c32c --- /dev/null +++ b/src/com/p4square/grow/model/ImageQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Image Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class ImageQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.IMAGE; + } +} diff --git a/src/com/p4square/grow/model/Point.java b/src/com/p4square/grow/model/Point.java new file mode 100644 index 0000000..e9fc0ca --- /dev/null +++ b/src/com/p4square/grow/model/Point.java @@ -0,0 +1,79 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple double based point class. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class Point { + /** + * Parse a comma separated x,y pair into a point. + * + * @return The point represented by the string. + * @throws IllegalArgumentException if the input is malformed. + */ + public static Point valueOf(String str) { + final int comma = str.indexOf(','); + if (comma == -1 || comma == 0 || comma == str.length() - 1) { + throw new IllegalArgumentException("Malformed point string"); + } + + final String sX = str.substring(0, comma); + final String sY = str.substring(comma + 1); + + return new Point(Double.valueOf(sX), Double.valueOf(sY)); + } + + private final double mX; + private final double mY; + + /** + * Create a new point with the given coordinates. + * + * @param x The x coordinate. + * @param y The y coordinate. + */ + public Point(double x, double y) { + mX = x; + mY = y; + } + + /** + * Compute the distance between this point and another. + * + * @param other The other point. + * @return The distance between this point and other. + */ + public double distance(Point other) { + final double dx = mX - other.mX; + final double dy = mY - other.mY; + + return Math.sqrt(dx*dx + dy*dy); + } + + /** + * @return The x coordinate. + */ + public double getX() { + return mX; + } + + /** + * @return The y coordinate. + */ + public double getY() { + return mY; + } + + /** + * @return The point represented as a comma separated pair. + */ + @Override + public String toString() { + return String.format("%.2f,%.2f", mX, mY); + } +} diff --git a/src/com/p4square/grow/model/QuadQuestion.java b/src/com/p4square/grow/model/QuadQuestion.java new file mode 100644 index 0000000..a7b4179 --- /dev/null +++ b/src/com/p4square/grow/model/QuadQuestion.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Two-dimensional Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuadQuestion extends Question { + private static final ScoringEngine ENGINE = new QuadScoringEngine(); + + private String mTop; + private String mRight; + private String mBottom; + private String mLeft; + + /** + * @return the top label. + */ + public String getTop() { + return mTop; + } + + /** + * Set the top label. + * @param s The new top label. + */ + public void setTop(String s) { + mTop = s; + } + + /** + * @return the right label. + */ + public String getRight() { + return mRight; + } + + /** + * Set the right label. + * @param s The new right label. + */ + public void setRight(String s) { + mRight = s; + } + + /** + * @return the bottom label. + */ + public String getBottom() { + return mBottom; + } + + /** + * Set the bottom label. + * @param s The new bottom label. + */ + public void setBottom(String s) { + mBottom = s; + } + + /** + * @return the left label. + */ + public String getLeft() { + return mLeft; + } + + /** + * Set the left label. + * @param s The new left label. + */ + public void setLeft(String s) { + mLeft = s; + } + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.QUAD; + } +} diff --git a/src/com/p4square/grow/model/QuadScoringEngine.java b/src/com/p4square/grow/model/QuadScoringEngine.java new file mode 100644 index 0000000..33403b5 --- /dev/null +++ b/src/com/p4square/grow/model/QuadScoringEngine.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import com.p4square.grow.model.Point; + +/** + * QuadScoringEngine expects the user's answer to be a Point string. We find + * the closest answer Point to the user's answer and treat that as the answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class QuadScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + // Find all of the answer points. + Point[] answers = new Point[question.getAnswers().size()]; + { + int i = 0; + for (String answerStr : question.getAnswers().keySet()) { + answers[i++] = Point.valueOf(answerStr); + } + } + + // Parse the user's answer. + Point userPoint = Point.valueOf(userAnswer.getAnswerId()); + + // Find the closest answer point to the user's answer. + double minDistance = Double.MAX_VALUE; + int answerIndex = 0; + for (int i = 0; i < answers.length; i++) { + final double distance = userPoint.distance(answers[i]); + if (distance < minDistance) { + minDistance = distance; + answerIndex = i; + } + } + + LOG.debug("Quad " + question.getId() + ": Got answer " + + answers[answerIndex].toString() + " for user point " + userAnswer); + + // Get the answer and update the score. + final Answer answer = question.getAnswers().get(answers[answerIndex].toString()); + return answer.score(score); + } +} diff --git a/src/com/p4square/grow/model/Question.java b/src/com/p4square/grow/model/Question.java index 387d723..37deffa 100644 --- a/src/com/p4square/grow/model/Question.java +++ b/src/com/p4square/grow/model/Question.java @@ -4,76 +4,125 @@ package com.p4square.grow.model; -import java.util.Collections; import java.util.HashMap; import java.util.Map; +import org.codehaus.jackson.annotate.JsonSubTypes; +import org.codehaus.jackson.annotate.JsonSubTypes.Type; +import org.codehaus.jackson.annotate.JsonTypeInfo; + /** * Model of an assessment question. * * @author Jesse Morgan <jesse@jesterpm.net> */ -public class Question { - public static enum QuestionType { - TEXT, IMAGE, SLIDER, QUAD, CIRCLE; +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + include = JsonTypeInfo.As.PROPERTY, + property = "type") +@JsonSubTypes({ + @Type(value = TextQuestion.class, name = "text"), + @Type(value = ImageQuestion.class, name = "image"), + @Type(value = SliderQuestion.class, name = "slider"), + @Type(value = QuadQuestion.class, name = "quad"), + @Type(value = CircleQuestion.class, name = "circle"), +}) +public abstract class Question { + /** + * QuestionType indicates the type of Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ + public enum QuestionType { + TEXT, + IMAGE, + SLIDER, + QUAD, + CIRCLE; + + @Override + public String toString() { + return name().toLowerCase(); + } } - private final Map<String, Object> mMap; - private final String mQuestionId; - private final QuestionType mType; - private final String mQuestionText; + private String mQuestionId; + private QuestionType mType; + private String mQuestionText; private Map<String, Answer> mAnswers; - private final String mPreviousQuestionId; - private final String mNextQuestionId; - - public Question(final Map<String, Object> map) { - mMap = map; - mQuestionId = (String) map.get("id"); - mType = QuestionType.valueOf(((String) map.get("type")).toUpperCase()); - - mQuestionText = (String) map.get("text"); - - mPreviousQuestionId = (String) map.get("previousQuestion"); - mNextQuestionId = (String) map.get("nextQuestion"); + private String mPreviousQuestionId; + private String mNextQuestionId; + public Question() { mAnswers = new HashMap<String, Answer>(); - for (Map.Entry<String, Object> answer : - ((Map<String, Object>) map.get("answers")).entrySet()) { - - final String id = answer.getKey(); - final Map<String, Object> answerMap = (Map<String, Object>) answer.getValue(); - final Answer answerObj = new Answer(id, answerMap); - mAnswers.put(id, answerObj); - } } + /** + * @return the id String for this question. + */ public String getId() { return mQuestionId; } - public QuestionType getType() { - return mType; + /** + * Set the id String for this question. + * @param id New id + */ + public void setId(String id) { + mQuestionId = id; } - public String getText() { + /** + * @return The Question text. + */ + public String getQuestion() { return mQuestionText; } + /** + * Set the question text. + * @param value The new question text. + */ + public void setQuestion(String value) { + mQuestionText = value; + } + + /** + * @return The id String of the previous question or null if no previous question exists. + */ public String getPreviousQuestion() { return mPreviousQuestionId; } + /** + * Set the id string of the previous question. + * @param id Previous question id or null if there is no previous question. + */ + public void setPreviousQuestion(String id) { + mPreviousQuestionId = id; + } + + /** + * @return The id String of the next question or null if no next question exists. + */ public String getNextQuestion() { return mNextQuestionId; } - public Map<String, Answer> getAnswers() { - return Collections.unmodifiableMap(mAnswers); + /** + * Set the id string of the next question. + * @param id next question id or null if there is no next question. + */ + public void setNextQuestion(String id) { + mNextQuestionId = id; } - public Map<String, Object> getMap() { - return Collections.unmodifiableMap(mMap); + /** + * @return a map of Answer id Strings to Answer objects. + */ + public Map<String, Answer> getAnswers() { + return mAnswers; } /** @@ -98,4 +147,19 @@ public class Question { return nextQuestion; } + + /** + * Update the score based on the answer to this question. + * + * @param score The running score to update. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, RecordedAnswer answer); + + /** + * @return the QuestionType of this question. + */ + public abstract QuestionType getType(); + } diff --git a/src/com/p4square/grow/model/RecordedAnswer.java b/src/com/p4square/grow/model/RecordedAnswer.java new file mode 100644 index 0000000..7d9905d --- /dev/null +++ b/src/com/p4square/grow/model/RecordedAnswer.java @@ -0,0 +1,34 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple model for a user's assessment answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class RecordedAnswer { + private String mAnswerId; + + /** + * @return The user's answer. + */ + public String getAnswerId() { + return mAnswerId; + } + + /** + * Set the answer id field. + * @param id The new id. + */ + public void setAnswerId(String id) { + mAnswerId = id; + } + + @Override + public String toString() { + return mAnswerId; + } +} diff --git a/src/com/p4square/grow/model/Score.java b/src/com/p4square/grow/model/Score.java new file mode 100644 index 0000000..b6deda2 --- /dev/null +++ b/src/com/p4square/grow/model/Score.java @@ -0,0 +1,70 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Simple structure containing a score's sum and count. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class Score { + /** + * Return the integer value for the given Score String. + */ + public static int numericScore(String score) { + if ("teacher".equals(score)) { + return 4; + } else if ("disciple".equals(score)) { + return 3; + } else if ("believer".equals(score)) { + return 2; + } else { + return 1; + } + } + + double sum; + int count; + + /** + * @return The sum of all the points. + */ + public double getSum() { + return sum; + } + + /** + * @return The number of questions included in the score. + */ + public int getCount() { + return count; + } + + /** + * @return The final score. + */ + public double getScore() { + return sum / count; + } + + @Override + public String toString() { + final double score = getScore(); + + if (score >= 4) { + return "teacher"; + + } else if (score >= 3) { + return "disciple"; + + } else if (score >= 2) { + return "believer"; + + } else { + return "seeker"; + } + } + +} diff --git a/src/com/p4square/grow/model/ScoringEngine.java b/src/com/p4square/grow/model/ScoringEngine.java new file mode 100644 index 0000000..8ff18b3 --- /dev/null +++ b/src/com/p4square/grow/model/ScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +import org.apache.log4j.Logger; + +/** + * ScoringEngine computes the score for a question and a given answer. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public abstract class ScoringEngine { + protected static final Logger LOG = Logger.getLogger(ScoringEngine.class); + + /** + * Update the score based on the given question and answer. + * + * @param score The running score to update. + * @param question The question to compute the score for. + * @param answer The answer give to this question. + * @return true if scoring should continue, false if this answer trumps everything else. + */ + public abstract boolean scoreAnswer(Score score, Question question, RecordedAnswer answer); +} diff --git a/src/com/p4square/grow/model/SimpleScoringEngine.java b/src/com/p4square/grow/model/SimpleScoringEngine.java new file mode 100644 index 0000000..6ef2dbb --- /dev/null +++ b/src/com/p4square/grow/model/SimpleScoringEngine.java @@ -0,0 +1,26 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SimpleScoringEngine expects the user's answer to a valid answer id and + * scores accordingly. + * + * If the answer id is not valid an Exception is thrown. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SimpleScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + final Answer answer = question.getAnswers().get(userAnswer.getAnswerId()); + if (answer == null) { + throw new IllegalArgumentException("Not a valid answer."); + } + + return answer.score(score); + } +} diff --git a/src/com/p4square/grow/model/SliderQuestion.java b/src/com/p4square/grow/model/SliderQuestion.java new file mode 100644 index 0000000..f0861e3 --- /dev/null +++ b/src/com/p4square/grow/model/SliderQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Slider Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SliderQuestion extends Question { + private static final ScoringEngine ENGINE = new SliderScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.SLIDER; + } +} diff --git a/src/com/p4square/grow/model/SliderScoringEngine.java b/src/com/p4square/grow/model/SliderScoringEngine.java new file mode 100644 index 0000000..76811b3 --- /dev/null +++ b/src/com/p4square/grow/model/SliderScoringEngine.java @@ -0,0 +1,29 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * SliderScoringEngine expects the user's answer to be a decimal value in the + * range [0, 1]. The value is scaled to the range [1, 4] and added to the + * score. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class SliderScoringEngine extends ScoringEngine { + + @Override + public boolean scoreAnswer(Score score, Question question, RecordedAnswer userAnswer) { + float delta = Float.valueOf(userAnswer.getAnswerId()) * 3 + 1; + + if (delta < 0 || delta > 4) { + throw new IllegalArgumentException("Answer out of bounds."); + } + + score.sum += delta; + score.count++; + + return true; + } +} diff --git a/src/com/p4square/grow/model/TextQuestion.java b/src/com/p4square/grow/model/TextQuestion.java new file mode 100644 index 0000000..88c2a34 --- /dev/null +++ b/src/com/p4square/grow/model/TextQuestion.java @@ -0,0 +1,24 @@ +/* + * Copyright 2013 Jesse Morgan + */ + +package com.p4square.grow.model; + +/** + * Text Question. + * + * @author Jesse Morgan <jesse@jesterpm.net> + */ +public class TextQuestion extends Question { + private static final ScoringEngine ENGINE = new SimpleScoringEngine(); + + @Override + public boolean scoreAnswer(Score score, RecordedAnswer answer) { + return ENGINE.scoreAnswer(score, this, answer); + } + + @Override + public QuestionType getType() { + return QuestionType.TEXT; + } +} |