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 |
|
---|
11 | import tudelft.healthpsychology.traumaontologies.questiontypes.QuestionType;
|
---|
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
|
---|
111 | public QuestionType getOptions(
|
---|
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 | }
|
---|