Skip to content

Commit

Permalink
Merge pull request ResearchKit#13 from Robert-Kolmos/strategy-based-n…
Browse files Browse the repository at this point in the history
…avigator

Strategy based navigator
  • Loading branch information
liujoshua authored Apr 27, 2018
2 parents 27cffd8 + bd0c02c commit 6e2a17d
Show file tree
Hide file tree
Showing 9 changed files with 1,091 additions and 78 deletions.
Binary file modified .idea/caches/build_file_checksums.ser
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
package org.sagebionetworks.research.domain.result;

import android.support.annotation.NonNull;
import android.support.annotation.Nullable;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import java.util.ArrayList;
Expand Down Expand Up @@ -63,6 +65,23 @@ public TaskResult(@NonNull final String identifier, @NonNull UUID taskRunUUID, @
this.asyncResults = ImmutableSet.copyOf(asyncResults);
}

/**
* Returns the first result in the step history with the given identifier.
* @param identifier The identifier to search for in the step history.
* @return the first result in the step history with the given identifier, or null if the
* identifier isn't found in the step history.
*/
@Nullable
public Result getResult(String identifier) {
for (Result result : this.stepHistory) {
if (result.getIdentifier().equals(identifier)) {
return result;
}
}

return null;
}

@NonNull
public UUID getTaskRunUUID() {
return taskRunUUID;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,5 @@ interface StepNavigator {
* @param taskResult current step result
* @return progress within the task
*/
fun getProgress(step: Step, taskResult: TaskResult): Progress
fun getProgress(step: Step, taskResult: TaskResult): Progress?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,336 @@
package org.sagebionetworks.research.domain.task.navigation;

import android.support.annotation.NonNull;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.sagebionetworks.research.domain.result.Result;
import org.sagebionetworks.research.domain.result.TaskResult;
import org.sagebionetworks.research.domain.step.SectionStep;
import org.sagebionetworks.research.domain.step.Step;
import org.sagebionetworks.research.domain.task.Task;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;

public class TreeNavigator implements StepNavigator {

// Stores the list of progressMarkers which are the identifiers of the steps which count
// towards computing the progress. An empty list represents the absence of progress markers,
// while null represents that the navigator should attempt to estimate the progress without the
// use of any progress markers.
@Nullable
private final ImmutableList<String> progressMarkers;

// The root of the tree navigator.
@NonNull
private final TreeNavigator.Node root;

// A map from step identifier to step.
@NonNull
private final ImmutableMap<String, Step> stepsById;

/**
* Constructs a TreeNavigator from the given list of steps, and the given progress markers
* @param steps The list of steps to construct this TreeNavigator from.
* @param progressMarkers The list of progressMarkers to construct this TreeNavigator from.
*/
public TreeNavigator(@NotNull List<Step> steps, @Nullable List<String> progressMarkers) {
this.root = new Node(steps);
if (progressMarkers == null) {
this.progressMarkers = null;
} else {
ImmutableList.Builder<String> builder = new ImmutableList.Builder<>();
builder.addAll(progressMarkers);
this.progressMarkers = builder.build();
}
this.stepsById = buildStepsByID(steps);
}

/**
* Constructs and returns an ImmutableMap that maps step identifiers to steps for every step
* and every substep of every step recursively, in the given list of steps.
* @param steps The list of steps to construct the map from.
* @return An ImmutableMap that maps step identifiers to steps for every step and every substep
* of every step recursively, in the given list of steps.
*/
private static ImmutableMap<String, Step> buildStepsByID(List<Step> steps) {
ImmutableMap.Builder<String, Step> builder = new ImmutableMap.Builder<>();
for (Step step : steps) {
addStepToBuilderRecursively(step, builder);
}

return builder.build();
}

/**
* Adds the given step and all of its substeps to the given builder.
* @param step The step to add (along with all of its substeps) to the given builder.
* @param builder The builder to add the given step to.
*/
private static void addStepToBuilderRecursively(Step step, ImmutableMap.Builder<String, Step> builder) {
if (step instanceof SectionStep) {
SectionStep sectionStep = (SectionStep)step;
builder.put(sectionStep.getIdentifier(), sectionStep);
for (Step childStep : sectionStep.getSteps()) {
addStepToBuilderRecursively(childStep, builder);
}
} else {
builder.put(step.getIdentifier(), step);
}
}

@Nullable
@Override
public Step getStep(@NotNull String identifier) {
return this.stepsById.get(identifier);
}

@Nullable
@Override
public Step getNextStep(@Nullable Step step, @NotNull TaskResult taskResult) {
return nextStepHelper(step, this.root, new AtomicBoolean(false));
}

/**
* Returns the first leaf step that appears after the given initialStep, in this TreeNavigator.
* After is defined as the next leaf in a pre-order traversal of the tree.
* @param initialStep The step to find the step after.
* @param current The step that is being evaluated by this call to nextStepHelper().
* @param hasFoundInitial True if the initialStep has already been encountered by a parent
* recursive call of this call to nextStepHelper(), false otherwise.
* @return The first leaf step that appears after the given initialStep, or null if no such
* step exists.
*/
@Nullable
private static Step nextStepHelper(@Nullable Step initialStep, @Nullable Node current,
AtomicBoolean hasFoundInitial) {
if (current == null) {
return null;
}

if (current.step != null) {
if (hasFoundInitial.get() && current.isLeaf()) {
return current.step;
}

if (current.step.equals(initialStep)) {
hasFoundInitial.set(true);
}
}

if (current.children != null) {
for (Node child : current.children) {
Step found = nextStepHelper(initialStep, child, hasFoundInitial);
if (found != null) {
return found;
}
}
}

return null;
}

@Nullable
@Override
public Step getPreviousStep(@NotNull Step step, @NotNull TaskResult taskResult) {
return previousStepHelper(step, this.root, new AtomicBoolean(false));
}

/**
* Returns the first leaf step that appears before the given initialStep, in this TreeNavigator.
* Before is defined as the next leaf in a reverse pre-order traversal of the tree.
* @param initialStep The step to find the step before.
* @param current The step that is being evaluated by this call to previousStepHelper().
* @param hasFoundInitial True if the initialStep has already been encountered by a parent
* recursive call of this call to previousStepHelper(), false otherwise.
* @return The first leaf step that appears before the given initialStep, or null if no such
* step exists.
*/
@Nullable
private static Step previousStepHelper(@Nullable Step initialStep, @Nullable Node current,
AtomicBoolean hasFoundInitial) {
if (current == null) {
return null;
}

if (current.step != null) {
if (hasFoundInitial.get() && current.isLeaf()) {
return current.step;
}

if (current.step.equals(initialStep)) {
hasFoundInitial.set(true);
}
}

if (current.children != null) {
for (Node child : current.children.reverse()) {
Step found = previousStepHelper(initialStep, child, hasFoundInitial);
if (found != null) {
return found;
}
}
}

return null;
}

@Nullable
@Override
public Task.Progress getProgress(@NonNull final Step step, @NonNull TaskResult taskResult) {
if (this.progressMarkers != null) {
// Get the list of steps that have executed, and add the current step in case it hasn't
// been added to the step history yet.
List<String> stepIdentifiers = new ArrayList<>();
for (Result result : taskResult.getStepHistory()) {
stepIdentifiers.add(result.getIdentifier());
}
stepIdentifiers.add(step.getIdentifier());

int idx = this.getLastIndexInMarkers(stepIdentifiers);
if (idx == -1) {
return null;
}

int current = idx + 1;
if (current == this.progressMarkers.size() &&
!this.progressMarkers.contains(step.getIdentifier())) {
// If this is the last step in the progress markers and we are beyond the step we
// return null.
return null;
}

// The progress is the current step index, out of the total number of markers, and it
// isn't estimated.
return new Task.Progress(current, this.progressMarkers.size(), false);
}

// If there are no progress markers, default to using the total number of steps, and the
// result to figure out how many have gone.
ImmutableSet<String> stepIDs = this.stepsById.keySet();
Set<String> finishedStepIDs = new HashSet<>();
for (Result result : taskResult.getStepHistory()) {
finishedStepIDs.add(result.getIdentifier());
}

// The union total will store the number of elements in both sets.
int unionTotal = 0;
for (String stepID: finishedStepIDs) {
if (stepIDs.contains(stepID)) {
unionTotal++;
}
}

// The number of unique elements across both sets is the total.
int total = stepIDs.size() + finishedStepIDs.size() - unionTotal;
// The current step hasn't been finished so we remove it.
finishedStepIDs.remove(step.getIdentifier());
// We add one here because the progress should be 1 indexed.
int current = finishedStepIDs.size() + 1;
return new Task.Progress(current, total, true);

}

/**
* Returns the last index in the progress markers such that the given list of identifiers
* contains the progress marker at this index.
* @param identifiers The list of identifiers to check against the progress markers.
* @return The last index in the progress markers such that the given list of identifiers
* contains the progress marker at this index.
*/
private int getLastIndexInMarkers(List<String> identifiers) {
if (this.progressMarkers != null) {
for (int i = this.progressMarkers.size() - 1; i >= 0; i--) {
if (identifiers.contains(progressMarkers.get(i))) {
return i;
}
}
}

return -1;
}

/*
* A class to represent a single node in the TreeNavigator
*/
private static final class Node {
// The step that this node represents
@Nullable
private final Step step;

// The children if any that this node has. children.isEmpty() == false always, when
// children == null the node is a leaf.
@Nullable
private final ImmutableList<Node> children;

/**
* Constructs a new node from the given list of steps. This node will represent the root
* of a Tree in which the steps are the children.
* @param steps The list of steps to construct the node from.
*/
private Node(@Nullable List<Step> steps) {
this.step = null;
this.children = constructChildNodes(steps);
}

/**
* Constructs a new node corresponding to the given step.
* @param step the step to construct the node from.
*/
private Node(@NonNull Step step) {
this.step = step;
if (step instanceof SectionStep) {
this.children = constructChildNodes(((SectionStep)step).getSteps());
} else {
this.children = null;
}
}

/**
* Constructs nodes from all the steps in the given list and returns them
* in an ImmutableList.
* @param childSteps the list of steps to construct nodes from.
* @return An ImmutableList of nodes constructed from the given steps.
*/
private ImmutableList<Node> constructChildNodes(@Nullable List<Step> childSteps) {
if (childSteps != null && !childSteps.isEmpty()) {
List<Node> children = new ArrayList<>();
for (Step childStep : childSteps) {
children.add(new Node(childStep));
}

ImmutableList.Builder<Node> builder = new ImmutableList.Builder<>();
builder.addAll(children);
return builder.build();
}

return null;
}


/**
* Returns true if this node is a leaf, false otherwise.
* @return true if this node is a leaf, false otherwise.
*/
private boolean isLeaf() {
return this.children == null;
}

@Override
public String toString() {
if (this.step != null) {
return this.step.toString() + ": " + "NODE";
} else {
return "ROOT";
}
}
}
}
Loading

0 comments on commit 6e2a17d

Please sign in to comment.