[4] | 1 | package tudelft.healthpsychology.traumaontologies.answerstate;
|
---|
| 2 |
|
---|
| 3 | import java.util.Collection;
|
---|
| 4 | import java.util.List;
|
---|
| 5 | import java.util.Optional;
|
---|
| 6 | import java.util.stream.Collectors;
|
---|
| 7 |
|
---|
| 8 | import com.fasterxml.jackson.annotation.JsonCreator;
|
---|
| 9 | import com.fasterxml.jackson.annotation.JsonProperty;
|
---|
| 10 |
|
---|
[5] | 11 | import tudelft.healthpsychology.traumaontologies.questiontypes.TypedQuestion;
|
---|
[4] | 12 | import tudelft.healthpsychology.traumaontologies.questiontypes.SelectFromList;
|
---|
| 13 | import tudelft.utilities.tree.Tree;
|
---|
| 14 |
|
---|
| 15 | /**
|
---|
| 16 | * This AnswerState searches the ontology class that is the best match to what
|
---|
| 17 | * the user has in mind. It tries to get as close as possible to the leaf nodes
|
---|
| 18 | * in the ontology, offering the user to choose between children nodes
|
---|
| 19 | * repeatedly.
|
---|
| 20 | *
|
---|
| 21 | * The state transitioning works as follows. The idea is that the initial state
|
---|
| 22 | * is with the most abstract {@link OntologyNode} fitting the expectations
|
---|
| 23 | * (typically "object" or "person").
|
---|
| 24 | *
|
---|
| 25 | * {@link #getOptions()} returns a set of more accurate ontology nodes that are
|
---|
| 26 | * children of the current node. By calling {@link #with(String)} with one of
|
---|
| 27 | * these nodes, the new state is that selected node. If tnat selected node is a
|
---|
| 28 | * leaf node, the state is final and the process is done.
|
---|
| 29 | *
|
---|
| 30 | * If with() is called with null it means "anders"/"other", which indicates the
|
---|
| 31 | * answer is not in the node set. If that is selected:
|
---|
| 32 | * <ul>
|
---|
| 33 | * <li>user did not yet select anything else and we are at depth >1. Then we
|
---|
| 34 | * decrease depth by 1.
|
---|
| 35 | * <li>user previously selected a child or we are at depth 1. Then we select the
|
---|
| 36 | * currently current node.
|
---|
| 37 | * </ul>
|
---|
| 38 | * immutable.
|
---|
| 39 | */
|
---|
| 40 | public class OntologyAnswerState implements AnswerState {
|
---|
| 41 | /**
|
---|
| 42 | * Max nr of options given to the user to choose from
|
---|
| 43 | */
|
---|
| 44 | private static final int MAX_OPTIONS = 15;
|
---|
| 45 | private static final String OTHER = "anders";
|
---|
| 46 | private static final String IT = "het";
|
---|
| 47 |
|
---|
| 48 | private final String nodelabel;
|
---|
| 49 | private final int depth;
|
---|
| 50 | private final boolean isFocused;
|
---|
| 51 | private final boolean isFinal;
|
---|
| 52 | private final String nameOfObject;
|
---|
| 53 |
|
---|
| 54 | /**
|
---|
| 55 | *
|
---|
| 56 | * @param qnodelabel the {@link OntologyNode} that needs to be more precise.
|
---|
| 57 | * @param depth the depth at which the options are selected. 1 means
|
---|
| 58 | * direct children, 2 means children of the children, etc.
|
---|
| 59 | * This can be shallower than the maximum depth if user
|
---|
| 60 | * selects "other" indicating his choice is not in the
|
---|
| 61 | * exact list.
|
---|
| 62 | * @param isFocused true iff the user made a focusing step - he selected
|
---|
| 63 | * something else than "other" at some point.
|
---|
| 64 | * @param isfinal true iff this is a final state where the user made his
|
---|
| 65 | * choice. This can be either due to selecting a leaf
|
---|
| 66 | * node, or due to selecting other and then again the same
|
---|
| 67 | * subtype, and therefore has to be recorded separately.
|
---|
| 68 | * @pram nameOfObject a name of the object, to make the question to the user
|
---|
| 69 | * more specific which is especially relevant if this is part of a
|
---|
| 70 | * breath-first and the user entered some name or so some time before.
|
---|
| 71 | * If null, "het" ("it") is used.
|
---|
| 72 | */
|
---|
| 73 |
|
---|
| 74 | @JsonCreator
|
---|
| 75 | public OntologyAnswerState(@JsonProperty("nodelabel") String qnodelabel,
|
---|
| 76 | @JsonProperty("depth") int depth,
|
---|
| 77 | @JsonProperty("isFocused") boolean isFocused,
|
---|
| 78 | @JsonProperty("isFinal") boolean isfinal,
|
---|
| 79 | @JsonProperty("nameOfObject") String nameOfObject) {
|
---|
| 80 | if (qnodelabel == null) {
|
---|
| 81 | throw new IllegalArgumentException("node must be not null");
|
---|
| 82 | }
|
---|
| 83 | this.nodelabel = qnodelabel;
|
---|
| 84 | this.depth = depth;
|
---|
| 85 | this.isFinal = isfinal;
|
---|
| 86 | this.isFocused = isFocused;
|
---|
| 87 | this.nameOfObject = nameOfObject == null ? IT : nameOfObject;
|
---|
| 88 | }
|
---|
| 89 |
|
---|
| 90 | /**
|
---|
| 91 | * Convenience constructor that sets depth to maximum for initial state.
|
---|
| 92 | *
|
---|
| 93 | * @param qnode the node to create a new AnswerState for. This is initially
|
---|
| 94 | * the most abstract OntologyNode that fits the range of
|
---|
| 95 | * possibilities, eg #Persoon. The answer process gradually
|
---|
| 96 | * focuses on more detailed children of this node, and qnode
|
---|
| 97 | * will then change to this more detailed choice.
|
---|
| 98 | *
|
---|
| 99 | * The depth is set to the maximum possible depth with
|
---|
| 100 | * {@link #MAX_OPTIONS} answers.
|
---|
| 101 | * @pram nameOfObject a name of the object, to make the question to the user
|
---|
| 102 | * more specific which is especially relevant if this is part of a
|
---|
| 103 | * breath-first and the user entered some name or so some time before.
|
---|
| 104 | * If null, "het" ("it") is used.
|
---|
| 105 | */
|
---|
| 106 | public OntologyAnswerState(OntologyNode qnode, String nameOfObject) {
|
---|
| 107 | this(qnode.getLabel(), maximumDepth(qnode), false, false, nameOfObject);
|
---|
| 108 | }
|
---|
| 109 |
|
---|
| 110 | @Override
|
---|
[5] | 111 | public TypedQuestion getOptions(
|
---|
[4] | 112 | Tree<String, Collection<Property>, OntologyNode> tree) {
|
---|
| 113 | if (isFinal)
|
---|
| 114 | return null;
|
---|
| 115 | OntologyNode node = tree.get(nodelabel);
|
---|
| 116 | List<String> optionstrings = node.getChildren(depth).stream()
|
---|
| 117 | .map(child -> child.getLabel()).collect(Collectors.toList());
|
---|
| 118 | optionstrings.add(OTHER);
|
---|
| 119 | String optionstring = optionstrings.toString().replace("[", "{")
|
---|
| 120 | .replace("]", "}");
|
---|
| 121 |
|
---|
| 122 | return new SelectFromList(
|
---|
| 123 | "Kan U " + (isFocused ? "nog specifieker " : "")
|
---|
| 124 | + "aangeven: wat voor soort " + nodelabel + " was "
|
---|
| 125 | + nameOfObject + "? " + optionstring,
|
---|
| 126 | optionstrings, node.getLabel());
|
---|
| 127 | }
|
---|
| 128 |
|
---|
| 129 | @Override
|
---|
| 130 | public OntologyAnswerState with(String answer,
|
---|
| 131 | Tree<String, Collection<Property>, OntologyNode> tree) {
|
---|
| 132 | if (OTHER.equals(answer)) {
|
---|
| 133 | return with((OntologyNode) null, tree);
|
---|
| 134 | }
|
---|
| 135 | OntologyNode root = tree.get(nodelabel);
|
---|
| 136 |
|
---|
| 137 | Optional<OntologyNode> selection = root.getChildren(depth).stream()
|
---|
| 138 | .filter(node -> answer.equals(node.getLabel())).findFirst();
|
---|
| 139 | if (!selection.isPresent()) {
|
---|
| 140 | throw new IllegalArgumentException(
|
---|
| 141 | "Antwoord " + answer + " is geen optie");
|
---|
| 142 | }
|
---|
| 143 | return with(selection.get(), tree);
|
---|
| 144 |
|
---|
| 145 | }
|
---|
| 146 |
|
---|
| 147 | /**
|
---|
| 148 | *
|
---|
| 149 | * @param choice the {@link OntologyAnswerState} state that advances this to
|
---|
| 150 | * the next state. If null, it means the user could not make a
|
---|
| 151 | * choice ("other"). <br>
|
---|
| 152 | * The returned state is created as follows.
|
---|
| 153 | * <ol>
|
---|
| 154 | * <li>if choice=null:
|
---|
| 155 | * <ol>
|
---|
| 156 | * <li>if parent=null: new state = this and is final.
|
---|
| 157 | * <li>else new state = parent, depth =1
|
---|
| 158 | * </ol>
|
---|
| 159 | *
|
---|
| 160 | * <li>if choice is a leaf node: new state=choice and is final
|
---|
| 161 | * <li>if choice=node we just came from : new state=choice and
|
---|
| 162 | * final.
|
---|
| 163 | * <li>else new state = choice, depth = maximum with 15-node
|
---|
| 164 | * limit
|
---|
| 165 | * </ol>
|
---|
| 166 | * @return new {@link OntologyAnswerState} reflecting that the choice was
|
---|
| 167 | * made. The explanation is cleared, assuming that the user has read
|
---|
| 168 | * and understood the explanation after he gave an answer.
|
---|
| 169 | * @throws IllegalStateException if {@link #isFinal()}
|
---|
| 170 | */
|
---|
| 171 | public OntologyAnswerState with(OntologyNode choice,
|
---|
| 172 | Tree<String, Collection<Property>, OntologyNode> tree) {
|
---|
| 173 | if (isFinal) {
|
---|
| 174 | throw new IllegalStateException("state is final");
|
---|
| 175 | }
|
---|
| 176 | OntologyNode node = tree.get(nodelabel);
|
---|
| 177 | if (choice == null) {
|
---|
| 178 | if (depth <= 1 || isFocused) {
|
---|
| 179 | // take the current node as answer
|
---|
| 180 | return new OntologyAnswerState(nodelabel, depth, isFocused,
|
---|
| 181 | true, nameOfObject);
|
---|
| 182 | }
|
---|
| 183 | // zoom out
|
---|
| 184 | return new OntologyAnswerState(nodelabel, depth - 1, false, false,
|
---|
| 185 | nameOfObject);
|
---|
| 186 | }
|
---|
| 187 |
|
---|
| 188 | // if choice has no children then this choice is also final.
|
---|
| 189 | return new OntologyAnswerState(choice.getLabel(), depth, true,
|
---|
| 190 | choice.getChildren().isEmpty(), nameOfObject);
|
---|
| 191 | }
|
---|
| 192 |
|
---|
| 193 | /**
|
---|
| 194 | * @return the specific node label that was reached at this point.
|
---|
| 195 | */
|
---|
| 196 | public String getNode() {
|
---|
| 197 | return nodelabel;
|
---|
| 198 | }
|
---|
| 199 |
|
---|
| 200 | /**
|
---|
| 201 | *
|
---|
| 202 | * @return the depth at which the options are selected. This can be
|
---|
| 203 | * shallower than the maximum depth, indicating we are restricting
|
---|
| 204 | * the user's answer options to non-leaf (more abstract) nodes.
|
---|
| 205 | */
|
---|
| 206 | public int getDepth() {
|
---|
| 207 | return depth;
|
---|
| 208 | }
|
---|
| 209 |
|
---|
| 210 | @Override
|
---|
| 211 | public String toString() {
|
---|
| 212 | return "OntologyAnswerState[" + nodelabel + "," + isFocused + ","
|
---|
| 213 | + depth + "," + isFinal + "," + nameOfObject + "]";
|
---|
| 214 | }
|
---|
| 215 |
|
---|
| 216 | /**
|
---|
| 217 | * @return the maximum depth achievable with at most MAX_ANSWERS. Assumes
|
---|
| 218 | * depth 1 and 2 exist and that #children <MAX_ANSWERS at depth 1.
|
---|
| 219 | */
|
---|
| 220 | private static int maximumDepth(OntologyNode node) {
|
---|
| 221 | List<OntologyNode> nodes = node.getChildren(1);
|
---|
| 222 | // assume at least 1 more child per depth step to put some upper limit
|
---|
| 223 | // invariant: up to given depth, there are less than MAX_OPTIONS
|
---|
| 224 | for (int depth = 1; depth < MAX_OPTIONS; depth++) {
|
---|
| 225 | List<OntologyNode> newnodes = node.getChildren(depth + 1);
|
---|
| 226 | // equals if we are effectively at the bottom of the tree.
|
---|
| 227 | if (newnodes.size() > MAX_OPTIONS || newnodes.equals(nodes))
|
---|
| 228 | return depth;
|
---|
| 229 | nodes = newnodes;
|
---|
| 230 | }
|
---|
| 231 | return MAX_OPTIONS;
|
---|
| 232 | }
|
---|
| 233 |
|
---|
| 234 | @Override
|
---|
| 235 | public int hashCode() {
|
---|
| 236 | final int prime = 31;
|
---|
| 237 | int result = 1;
|
---|
| 238 | result = prime * result + depth;
|
---|
| 239 | result = prime * result + (isFinal ? 1231 : 1237);
|
---|
| 240 | result = prime * result + (isFocused ? 1231 : 1237);
|
---|
| 241 | result = prime * result
|
---|
| 242 | + ((nameOfObject == null) ? 0 : nameOfObject.hashCode());
|
---|
| 243 | result = prime * result
|
---|
| 244 | + ((nodelabel == null) ? 0 : nodelabel.hashCode());
|
---|
| 245 | return result;
|
---|
| 246 | }
|
---|
| 247 |
|
---|
| 248 | @Override
|
---|
| 249 | public boolean equals(Object obj) {
|
---|
| 250 | if (this == obj)
|
---|
| 251 | return true;
|
---|
| 252 | if (obj == null)
|
---|
| 253 | return false;
|
---|
| 254 | if (getClass() != obj.getClass())
|
---|
| 255 | return false;
|
---|
| 256 | OntologyAnswerState other = (OntologyAnswerState) obj;
|
---|
| 257 | if (depth != other.depth)
|
---|
| 258 | return false;
|
---|
| 259 | if (isFinal != other.isFinal)
|
---|
| 260 | return false;
|
---|
| 261 | if (isFocused != other.isFocused)
|
---|
| 262 | return false;
|
---|
| 263 | if (nameOfObject == null) {
|
---|
| 264 | if (other.nameOfObject != null)
|
---|
| 265 | return false;
|
---|
| 266 | } else if (!nameOfObject.equals(other.nameOfObject))
|
---|
| 267 | return false;
|
---|
| 268 | if (nodelabel == null) {
|
---|
| 269 | if (other.nodelabel != null)
|
---|
| 270 | return false;
|
---|
| 271 | } else if (!nodelabel.equals(other.nodelabel))
|
---|
| 272 | return false;
|
---|
| 273 | return true;
|
---|
| 274 | }
|
---|
| 275 |
|
---|
| 276 | }
|
---|