package tudelft.healthpsychology.traumaontologies.answerstate;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import tudelft.healthpsychology.traumaontologies.questiontypes.TypedQuestion;
import tudelft.healthpsychology.traumaontologies.questiontypes.SelectFromList;
import tudelft.utilities.tree.Tree;
/**
* This AnswerState searches the ontology class that is the best match to what
* the user has in mind. It tries to get as close as possible to the leaf nodes
* in the ontology, offering the user to choose between children nodes
* repeatedly.
*
* The state transitioning works as follows. The idea is that the initial state
* is with the most abstract {@link OntologyNode} fitting the expectations
* (typically "object" or "person").
*
* {@link #getOptions()} returns a set of more accurate ontology nodes that are
* children of the current node. By calling {@link #with(String)} with one of
* these nodes, the new state is that selected node. If tnat selected node is a
* leaf node, the state is final and the process is done.
*
* If with() is called with null it means "anders"/"other", which indicates the
* answer is not in the node set. If that is selected:
*
* - user did not yet select anything else and we are at depth >1. Then we
* decrease depth by 1.
*
- user previously selected a child or we are at depth 1. Then we select the
* currently current node.
*
* immutable.
*/
public class OntologyAnswerState implements AnswerState {
/**
* Max nr of options given to the user to choose from
*/
private static final int MAX_OPTIONS = 15;
private static final String OTHER = "anders";
private static final String IT = "het";
private final String nodelabel;
private final int depth;
private final boolean isFocused;
private final boolean isFinal;
private final String nameOfObject;
/**
*
* @param qnodelabel the {@link OntologyNode} that needs to be more precise.
* @param depth the depth at which the options are selected. 1 means
* direct children, 2 means children of the children, etc.
* This can be shallower than the maximum depth if user
* selects "other" indicating his choice is not in the
* exact list.
* @param isFocused true iff the user made a focusing step - he selected
* something else than "other" at some point.
* @param isfinal true iff this is a final state where the user made his
* choice. This can be either due to selecting a leaf
* node, or due to selecting other and then again the same
* subtype, and therefore has to be recorded separately.
* @pram nameOfObject a name of the object, to make the question to the user
* more specific which is especially relevant if this is part of a
* breath-first and the user entered some name or so some time before.
* If null, "het" ("it") is used.
*/
@JsonCreator
public OntologyAnswerState(@JsonProperty("nodelabel") String qnodelabel,
@JsonProperty("depth") int depth,
@JsonProperty("isFocused") boolean isFocused,
@JsonProperty("isFinal") boolean isfinal,
@JsonProperty("nameOfObject") String nameOfObject) {
if (qnodelabel == null) {
throw new IllegalArgumentException("node must be not null");
}
this.nodelabel = qnodelabel;
this.depth = depth;
this.isFinal = isfinal;
this.isFocused = isFocused;
this.nameOfObject = nameOfObject == null ? IT : nameOfObject;
}
/**
* Convenience constructor that sets depth to maximum for initial state.
*
* @param qnode the node to create a new AnswerState for. This is initially
* the most abstract OntologyNode that fits the range of
* possibilities, eg #Persoon. The answer process gradually
* focuses on more detailed children of this node, and qnode
* will then change to this more detailed choice.
*
* The depth is set to the maximum possible depth with
* {@link #MAX_OPTIONS} answers.
* @pram nameOfObject a name of the object, to make the question to the user
* more specific which is especially relevant if this is part of a
* breath-first and the user entered some name or so some time before.
* If null, "het" ("it") is used.
*/
public OntologyAnswerState(OntologyNode qnode, String nameOfObject) {
this(qnode.getLabel(), maximumDepth(qnode), false, false, nameOfObject);
}
@Override
public TypedQuestion getOptions(
Tree, OntologyNode> tree) {
if (isFinal)
return null;
OntologyNode node = tree.get(nodelabel);
List optionstrings = node.getChildren(depth).stream()
.map(child -> child.getLabel()).collect(Collectors.toList());
optionstrings.add(OTHER);
String optionstring = optionstrings.toString().replace("[", "{")
.replace("]", "}");
return new SelectFromList(
"Kan U " + (isFocused ? "nog specifieker " : "")
+ "aangeven: wat voor soort " + nodelabel + " was "
+ nameOfObject + "? " + optionstring,
optionstrings, node.getLabel());
}
@Override
public OntologyAnswerState with(String answer,
Tree, OntologyNode> tree) {
if (OTHER.equals(answer)) {
return with((OntologyNode) null, tree);
}
OntologyNode root = tree.get(nodelabel);
Optional selection = root.getChildren(depth).stream()
.filter(node -> answer.equals(node.getLabel())).findFirst();
if (!selection.isPresent()) {
throw new IllegalArgumentException(
"Antwoord " + answer + " is geen optie");
}
return with(selection.get(), tree);
}
/**
*
* @param choice the {@link OntologyAnswerState} state that advances this to
* the next state. If null, it means the user could not make a
* choice ("other").
* The returned state is created as follows.
*
* - if choice=null:
*
* - if parent=null: new state = this and is final.
*
- else new state = parent, depth =1
*
*
* - if choice is a leaf node: new state=choice and is final
*
- if choice=node we just came from : new state=choice and
* final.
*
- else new state = choice, depth = maximum with 15-node
* limit
*
* @return new {@link OntologyAnswerState} reflecting that the choice was
* made. The explanation is cleared, assuming that the user has read
* and understood the explanation after he gave an answer.
* @throws IllegalStateException if {@link #isFinal()}
*/
public OntologyAnswerState with(OntologyNode choice,
Tree, OntologyNode> tree) {
if (isFinal) {
throw new IllegalStateException("state is final");
}
OntologyNode node = tree.get(nodelabel);
if (choice == null) {
if (depth <= 1 || isFocused) {
// take the current node as answer
return new OntologyAnswerState(nodelabel, depth, isFocused,
true, nameOfObject);
}
// zoom out
return new OntologyAnswerState(nodelabel, depth - 1, false, false,
nameOfObject);
}
// if choice has no children then this choice is also final.
return new OntologyAnswerState(choice.getLabel(), depth, true,
choice.getChildren().isEmpty(), nameOfObject);
}
/**
* @return the specific node label that was reached at this point.
*/
public String getNode() {
return nodelabel;
}
/**
*
* @return the depth at which the options are selected. This can be
* shallower than the maximum depth, indicating we are restricting
* the user's answer options to non-leaf (more abstract) nodes.
*/
public int getDepth() {
return depth;
}
@Override
public String toString() {
return "OntologyAnswerState[" + nodelabel + "," + isFocused + ","
+ depth + "," + isFinal + "," + nameOfObject + "]";
}
/**
* @return the maximum depth achievable with at most MAX_ANSWERS. Assumes
* depth 1 and 2 exist and that #children nodes = node.getChildren(1);
// assume at least 1 more child per depth step to put some upper limit
// invariant: up to given depth, there are less than MAX_OPTIONS
for (int depth = 1; depth < MAX_OPTIONS; depth++) {
List newnodes = node.getChildren(depth + 1);
// equals if we are effectively at the bottom of the tree.
if (newnodes.size() > MAX_OPTIONS || newnodes.equals(nodes))
return depth;
nodes = newnodes;
}
return MAX_OPTIONS;
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + depth;
result = prime * result + (isFinal ? 1231 : 1237);
result = prime * result + (isFocused ? 1231 : 1237);
result = prime * result
+ ((nameOfObject == null) ? 0 : nameOfObject.hashCode());
result = prime * result
+ ((nodelabel == null) ? 0 : nodelabel.hashCode());
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
OntologyAnswerState other = (OntologyAnswerState) obj;
if (depth != other.depth)
return false;
if (isFinal != other.isFinal)
return false;
if (isFocused != other.isFocused)
return false;
if (nameOfObject == null) {
if (other.nameOfObject != null)
return false;
} else if (!nameOfObject.equals(other.nameOfObject))
return false;
if (nodelabel == null) {
if (other.nodelabel != null)
return false;
} else if (!nodelabel.equals(other.nodelabel))
return false;
return true;
}
}