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: * * 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. *
    *
  1. if choice=null: *
      *
    1. if parent=null: new state = this and is final. *
    2. else new state = parent, depth =1 *
    * *
  2. if choice is a leaf node: new state=choice and is final *
  3. if choice=node we just came from : new state=choice and * final. *
  4. 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; } }