package org.geogebra.common.util;
import java.util.ArrayList;
import java.util.TreeSet;
import org.geogebra.common.kernel.Kernel;
import org.geogebra.common.kernel.Macro;
import org.geogebra.common.kernel.geos.GeoBoolean;
import org.geogebra.common.kernel.geos.GeoElement;
import org.geogebra.common.kernel.geos.GeoNumeric;
import org.geogebra.common.main.App;
/**
* An exercise containing the assignments
*
* @author Christoph
*
*/
public class Exercise {
private ArrayList<Assignment> assignments;
private Kernel kernel;
private App app;
/**
* Create a new Exercise
*
* @param app
* application
*/
public Exercise(App app) {
this.app = app;
kernel = app.getKernel();
assignments = new ArrayList<Assignment>();
}
/**
* Resets the Exercise to contain no user defined tools.
*/
public void reset() {
assignments = new ArrayList<Assignment>();
}
/**
* Resets the Exercise and adds all user defined tools to the Exercise.
*/
public void initStandardExercise() {
reset();
if (app.getKernel().hasMacros()) {
ArrayList<Macro> macros = app.getKernel().getAllMacros();
for (Macro macro : macros) {
addAssignment(macro);
}
}
}
/**
* Checks all Assignments in this exercise.
*
* Use getResult() and getHints
*/
public void checkExercise() {
if (assignments.isEmpty()) {
initStandardExercise();
}
// ArrayList<String> addListeners = app.getScriptManager()
// .getAddListeners();
// ArrayList<String> tmpListeners = new ArrayList<String>();
// for (String addListener : addListeners) {
// tmpListeners.add(addListener);
// app.getScriptManager().unregisterAddListener(addListener);
// }
app.getScriptManager().disableListeners();
for (Assignment assignment : assignments) {
// if (assignment.isValid()) {
assignment.checkAssignment();
// }
}
app.getScriptManager().enableListeners();
// for (String addListener : tmpListeners) {
// app.getScriptManager().registerAddListener(addListener);
// }
}
/**
* The overall fraction of the Exercise
*
* If one Assignment has 100%, the overall fraction will be 1 minus the sum
* of the fractions of the Assignments having negative Fractions.<br>
* Otherwise the overall fraction will be the sum of all positive fractions
* capped at 1 minus all negative fractions and then capped at 0.
*
* @return the sum of fractions for all assignments
*/
public double getFraction() {
double fractionsumplus = 0;
double fractionsumminus = 0;
Assignment singleCorrect = null;
double stdPrecision = Kernel.STANDARD_PRECISION;
for (Assignment assignment : assignments) {
double assignmenFraction = assignment.getFraction();
if (assignmenFraction >= 0) {
if (assignmenFraction >= 1 - stdPrecision) {
singleCorrect = assignment;
}
fractionsumplus += assignmenFraction;
} else {
fractionsumminus += assignmenFraction;
}
}
double fraction = 0;
if (singleCorrect != null || fractionsumplus >= 1 - stdPrecision) {
fraction = 1;
} else {
fraction = fractionsumplus;
}
fraction += fractionsumminus;
return fraction < 0 + stdPrecision ? 0 : fraction;
}
/**
* Creates a new Assignment and adds it to the Exercise.
*
* @param macro
* the user defined Tool corresponding to the Assignment
* @return the newly created Assignment
*/
public GeoAssignment addAssignment(Macro macro) {
GeoAssignment a = getAssignment(macro);
if (a == null) {
a = new GeoAssignment(macro);
addAssignment(a);
}
return a;
}
/**
* Creates a new Assignment and adds it to the Exercise.
*
* @param geo
* the GeoBoolean which should be used for the check
* @return the newly created Assignment
*/
public BoolAssignment addAssignment(GeoBoolean geo) {
BoolAssignment a = getAssignment(geo);
if (a == null) {
a = new BoolAssignment(geo, app.getKernel());
addAssignment(a);
}
return a;
}
/**
* Creates a new Assignment and adds it to the Exercise.
*
* @param geoBooleanLabel
* the label of the geoBoolean which should be used for the check
* @return the newly created Assignment
*/
public BoolAssignment addAssignment(String geoBooleanLabel) {
BoolAssignment a = new BoolAssignment(geoBooleanLabel, app.getKernel());
assignments.add(a);
return a;
}
/**
* @return all assignments contained in the exercise
*/
public ArrayList<Assignment> getParts() {
return assignments;
}
/**
* Check if a macro is already used by this exercise
*
* @param macro
* the user defined tool
* @return true if this exercise uses the macro
*/
public boolean usesMacro(Macro macro) {
boolean uses = false;
for (Assignment assignment : assignments) {
if (assignment instanceof GeoAssignment) {
uses = uses
|| ((GeoAssignment) assignment).getTool().equals(macro);
}
}
return uses;
}
/**
* Check if a macro is already used by this exercise
*
* @param macroID
* the id of the user defined tool
* @return {@link #usesMacro(Macro)}
*/
public boolean usesMacro(int macroID) {
return usesMacro(kernel.getMacro(macroID));
}
/**
* @param geo
* the GeoBoolean to check for
* @return true if the GeoBoolean is used by the Exercise
*/
public boolean usesBoolean(GeoBoolean geo) {
boolean uses = false;
for (Assignment assignment : assignments) {
if (assignment instanceof BoolAssignment) {
uses = uses
|| ((BoolAssignment) assignment).usesGeoBoolean(geo);
}
}
return uses;
}
/**
* @return false if there are no Macros or any change to the standard
* behavior has been made with the ExerciseBuilder <br />
* true if there are Macros which can be used for checking
*
*/
private boolean isStandardExercise() {
boolean res = true;
if (assignments.size() > 0) {
res = false;
}
for (int i = 0; i < assignments.size() && res; i++) {
if (assignments.get(i) instanceof GeoAssignment) {
res = ((GeoAssignment) assignments.get(i)).getTool()
.equals(app.getKernel().getAllMacros().get(i))
&& !(assignments.get(i).hasHint()
|| assignments.get(i).hasFraction()
|| !((GeoAssignment) assignments.get(i))
.getCheckOperation()
.equals("AreEqual"));
} else {
res = false;
}
}
return res;
}
/**
* @return XML describing the Exercise. Will be empty if no changes to the
* Exercise were made (i.e. if isStandardExercise).<br />
* Only Elements and Properties which are set or not standard will
* be included.
*
* <pre>
* {@code
* <assignment toolName="Tool2">
* <result name="CORRECT" hint="Great, that's correct!" />
* <result name="WRONG" hint="Try again!" />
* <result name="NOT_ENOUGH_INPUTS" hint="You should at least have {inputs} in your construction!" />
* <result name="WRONG_INPUT_TYPES" hint="We were not able to find {inputs}, although it seems you have drawn a triangle!" />
* <result name="WRONG_OUTPUT_TYPE" hint="We couldn't find a triangle in the construction!" />
* <result name="WRONG_AFTER_RANDOMIZE" hint="Should never happen in this construction! Contact your teacher!" fraction="0.5" />
* <result name="UNKNOWN" hint="Something went wrong - ask your teacher!" />
* </assignment>
* }
* </pre>
*/
public String getExerciseXML() {
StringBuilder sb = new StringBuilder();
if (!isStandardExercise()) {
for (Assignment a : assignments) {
sb.append(a.getAssignmentXML());
}
}
return sb.toString();
}
/**
* Check if the Exercise has assignments
*
* @return true if there are no assignments in this Exercise
*/
public boolean isEmpty() {
return assignments.isEmpty();
}
/**
* Remove an assignment from this Exercise
*
* @param assignment
* the assignment to be removed from the Exercise
*/
public void remove(Assignment assignment) {
assignments.remove(assignment);
}
/**
* Should be used if Macro is deleted
*
* @param macro
* the macro being used by the assignment which should be removed
*/
public void removeAssignment(Macro macro) {
Assignment assignmentToRemove = null;
for (Assignment assignment : assignments) {
if (assignment instanceof GeoAssignment) {
if (((GeoAssignment) assignment).getTool().equals(macro)) {
assignmentToRemove = assignment;
}
}
}
if (assignmentToRemove != null) {
remove(assignmentToRemove);
}
}
/**
* Removes all Assignments from the Exercise
*/
public void removeAllAssignments() {
reset();
}
/**
* @param macro
* the macro being used by the assignment which should be
* retrieved
* @return the assignment being used by the macro or null if macro isn't
* used by the Exercise
*/
public GeoAssignment getAssignment(Macro macro) {
GeoAssignment assignmentToReturn = null;
for (Assignment assignment : assignments) {
if (assignment instanceof GeoAssignment) {
if (((GeoAssignment) assignment).getTool().equals(macro)) {
assignmentToReturn = (GeoAssignment) assignment;
}
}
}
return assignmentToReturn;
}
/**
* @param geo
* the GeoBoolean being used by the assignment which should be
* retrieved
* @return the assignment which uses the geo or null if not used
*/
public BoolAssignment getAssignment(GeoBoolean geo) {
BoolAssignment assignmentToReturn = null;
for (Assignment assignment : assignments) {
if (assignment instanceof BoolAssignment) {
if (((BoolAssignment) assignment).usesGeoBoolean(geo)) {
assignmentToReturn = (BoolAssignment) assignment;
}
}
}
return assignmentToReturn;
}
/**
* Adds an assignment to the exercise
*
* @param assignment
* the assignment to add to the Exercise
*/
public void addAssignment(Assignment assignment) {
addAssignment(assignments.size(), assignment);
}
private boolean isValid(Assignment assignment) {
return !assignments.contains(assignment) && assignment.isValid();
}
/**
* Adds an assignment to the exercise at specified index
*
* @param assignmentIndex
* the index to be used for the assignment, shifts all Elments =>
* assignmentIndex to the right
* @param assignment
* the assignment to add to the Exercise
*/
public void addAssignment(int assignmentIndex, Assignment assignment) {
if (isValid(assignment)) {
assignments.add(assignmentIndex, assignment);
}
}
/**
* After a student started the Exercise in an CMS/LMS, values on which the
* solution depends should not get changed anymore, when reopening the file.
* If the exercise gets saved so that the teacher can review it or the
* student can work on it later again, the teacher as well as the student
* should see the same values the student saw on the first time. <br>
* If it doesn't get saved, this doesn't do any harm since the Assignment
* will be randomized again on next try.<br>
* The current values for displaying in CMS/LMS are also returned, so that
* the CMS/LMS can display the values and not cluttering up the applet with
* text, which can be shown more pretty in the CMS/LMS (most of the times).
*
* @return all random geos of type GeoNumeric on which a BoolAssignment is
* dependent and stops randomizing all of these values.
*/
public ArrayList<GeoNumeric> stopRandomizeAndGetValuesForBoolAssignments() {
// TODO Auto-generated method stub
ArrayList<GeoNumeric> geos = new ArrayList<GeoNumeric>();
TreeSet<GeoElement> predecessorsOfUsedBooleans = new TreeSet<GeoElement>();
for (Assignment assignment : assignments) {
if (assignment instanceof BoolAssignment) {
predecessorsOfUsedBooleans.addAll(((BoolAssignment) assignment)
.getGeoBoolean().getAllPredecessors());
}
}
for (GeoElement geo : predecessorsOfUsedBooleans) {
if (geo instanceof GeoNumeric && geo.isLabelSet()) {
geos.add((GeoNumeric) geo);
if (geo.isRandomGeo()) {
((GeoNumeric) geo).setRandom(false);
}
}
// If we also want to stop randomizing other values which were
// randomized by some AlgoRandom the user would have to specify the
// variables used by the assignment. I think this is not necessary
// since if we only stop randomization of
// sliders I think it's easier to use for the teacher.
}
return geos;
}
/**
* If undo happens we have to update the Exercise to the new GeoElements.
* Also used to remove invalid assignments before showing
* Exercisebuilderdialog
*/
public void notifyUpdate() {
for (Assignment assignment : assignments) {
if (assignment instanceof BoolAssignment) {
if (!((BoolAssignment) assignment).update()) {
assignments.remove(assignment);
}
}
}
}
}