Schedule.java
/*
* Copyright 2016 the Cook-E development team
*
* This file is part of Cook-E.
*
* Cook-E is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Cook-E is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Cook-E. If not, see <http://www.gnu.org/licenses/>.
*/
package org.cook_e.data;
import android.support.annotation.NonNull;
import android.util.Log;
import java.util.ArrayList;
import java.util.List;
/**
* A class that produces and manages a schedule for a Bunch.
*/
public class Schedule {
private final List<ScheduledStep> mScheduledStepList;
private final List<UnscheduledRecipeSteps> mUnscheduledRecipeStepsList;
private final int mTotalStepCount;
private final int mOriginalEstimatedTime;
private final int mOptimizedEstimatedTime;
private int mCurrScheduledStepIndex = -1;
/**
* Creates a schedule based on the given Bunch.
*
* @param b the Bunch to schedule steps from
*/
public Schedule(Bunch b) {
this(b, true);
}
/**
* Creates a schedule based on the given Bunch. If the given boolean is true,
* then estimated times are calculated, otherwise they aren't. This private
* constructor with the additional boolean is necessary to avoid infinite
* recursive calls due to calculating estimed times.
*
* @param b the Bunch to schedule steps from
* @param calculateEstimatedTimes whether or not estimated times should be calculated
*/
private Schedule(Bunch b, boolean calculateEstimatedTimes) {
if (calculateEstimatedTimes) {
this.mOriginalEstimatedTime = CookingTimeEstimator.getOriginalTime(b);
this.mOptimizedEstimatedTime = CookingTimeEstimator.getOptimizedTime(new Schedule(b, false));
} else {
this.mOriginalEstimatedTime = -1;
this.mOptimizedEstimatedTime = -1;
}
this.mScheduledStepList = new ArrayList<>();
int totalStepCount = 0;
List<Recipe> recipes = b.getRecipes();
for(Recipe recipe : recipes) {
totalStepCount += recipe.getSteps().size();
}
this.mTotalStepCount = totalStepCount;
// populate UnscheduledRecipeStepsList
this.mUnscheduledRecipeStepsList = new ArrayList<>();
for (Recipe r: recipes) {
if(r.getSteps().isEmpty()) continue;
this.mUnscheduledRecipeStepsList.add(new UnscheduledRecipeSteps(r));
}
}
/**
* Returns the recipe that contains the current step
* @return the recipe current step belongs to
*/
public Recipe getCurrentStepRecipe() {
return this.mScheduledStepList.get(this.mCurrScheduledStepIndex).motherRecipe;
}
/**
* This function returns the next step. Calling this function implies that
* the current step has been completed if it is a non-simultaneous task. If
* it a simultaneous task, then it is the callers job to call finishStep and
* pass in the Step when it has been completed.
*
* @return The next step after the current step. If it's already at the final step
* or no other steps can be scheduled, then null is returned.
*/
public Step getNextStep() {
Step nextStep = null;
if (this.mCurrScheduledStepIndex < this.mScheduledStepList.size() - 1) {
// handles the case where the next step has already
// been scheduled
this.mCurrScheduledStepIndex++;
nextStep = this.mScheduledStepList.get(this.mCurrScheduledStepIndex).step;
} else if (this.mCurrScheduledStepIndex == this.mScheduledStepList.size() -1 &&
this.mUnscheduledRecipeStepsList.size() > 0) {
// handles the case where the next step hasn't been
// scheduled yet
this.mCurrScheduledStepIndex++;
ScheduledStep nextScheduledStep = getNextScheduledStep(this.mUnscheduledRecipeStepsList);
this.mScheduledStepList.add(nextScheduledStep);
nextStep = nextScheduledStep.step;
}
return nextStep;
}
/**
* After done populating the final scheduled step list. This function will return the previous step
* of the current Step. If there is no previous step, then null is returned.
*
* @return The previous step before the current Step
*/
public Step getPrevStep() {
Step prevStep = null;
if (this.mCurrScheduledStepIndex > 0) {
this.mCurrScheduledStepIndex--;
prevStep = this.mScheduledStepList.get(this.mCurrScheduledStepIndex).step;
}
return prevStep;
}
/**
* Calling this function indicates that the blocking simultaneous step
* associated with the given recipe has been completed. If no matching recipe
* is found, then the function silently does nothing. This is largely due to
* how hard it would be for the caller to know if the particular recipe has
* has any unscheduled steps left.
*
* @param recipe the recipe the finished simultaneous step is associated with
*/
public void finishSimultaneousStepFromRecipe(Recipe recipe) {
UnscheduledRecipeSteps matchingRecipeSteps = null;
for (UnscheduledRecipeSteps currUnscheduledRecipeSteps : mUnscheduledRecipeStepsList) {
if (currUnscheduledRecipeSteps.motherReceipe.equals(recipe)) {
currUnscheduledRecipeSteps.setReady();
return;
}
}
}
/**
* Returns the total number of steps. This value includes both
* scheduled and unscheduled steps.
*
* @return the total number of steps
*/
public int getStepCount() {
return this.mTotalStepCount;
}
/*
* Removes and returns the step to schedule for shortest cooking time.
* The given finalSteps is also modified such that each element in the list
* has its busy time shifted properly.
*
* @param finalSteps the unscheduled finalSteps to pick a next step from
* @return the next step to schedule for shortest cooking time
*/
private ScheduledStep getNextScheduledStep(List<UnscheduledRecipeSteps> unscheduledRecipeStepsList) {
// Finds the recipe with the longest time from the first simultaneous step
// to the last step that is ready.
int chosenIndex = -1;
int maxSimultaneousToEndTime = -1;
for (int i = 0; i < unscheduledRecipeStepsList.size(); i++) {
UnscheduledRecipeSteps currSteps = unscheduledRecipeStepsList.get(i);
if (currSteps.isReady()) {
// if the recipe is ready, then check if it is the new best choice
// and update accordingly
int currSimultaneousToEndTime = currSteps.getSimultaneousToEndTime();
if (currSimultaneousToEndTime > maxSimultaneousToEndTime) {
chosenIndex = i;
maxSimultaneousToEndTime = currSimultaneousToEndTime;
}
}
}
Step nextScheduledStep = null;
Recipe motherRecipe = null;
if (chosenIndex != -1) {
// Handles case where one or more recipes were ready by removing and
// returning the chosen step.
motherRecipe = unscheduledRecipeStepsList.get(chosenIndex).motherReceipe;
nextScheduledStep = unscheduledRecipeStepsList.get(chosenIndex).removeNextStep();
Log.d("Schedule", "chosenIndex = " + chosenIndex + ", unscheduled steps = " + unscheduledRecipeStepsList);
if (unscheduledRecipeStepsList.get(chosenIndex).isEmpty()) {
Log.d("Schedule", "chosenIndex = " + chosenIndex + ", unscheduled steps = " + unscheduledRecipeStepsList);
unscheduledRecipeStepsList.remove(chosenIndex);
}
}
return new ScheduledStep(nextScheduledStep, motherRecipe);
}
/**
* A private helper class used in the process of generating
* the schedule.
*/
private class UnscheduledRecipeSteps {
// The list of unscheduled finalSteps.
private final List<Step> steps;
// The time in seconds from the first simultaneous step to the end of the last step.
private int simultaneousToEndTime;
// Whether or not the recipe is ready or not. A recipes isn't
// ready if a simultaneous step is in progress.
private boolean isReady;
public final Recipe motherReceipe;
/**
* Creates an UnscheduledRecipeSteps object based on the given Recipe.
*
* @param r the Recipe to get steps from
*/
public UnscheduledRecipeSteps(Recipe r) {
this.steps = r.getSteps();
this.isReady = true;
this.motherReceipe = r;
// initializes simultaneousToEndTime
this.simultaneousToEndTime = 0;
boolean simultaneousSeen = false;
for (Step currStep : this.steps) {
if (simultaneousSeen) {
this.simultaneousToEndTime += currStep.getDurationMinutes();
} else if (currStep.isSimultaneous()) {
simultaneousSeen = true;
this.simultaneousToEndTime = currStep.getDurationMinutes();
}
}
}
/**
* Returns the time in seconds from the first simultaneous step to the end of the last step.
*
* @return time in seconds from the first simultaneous step to the end of the last step.
*/
public int getSimultaneousToEndTime() {
return simultaneousToEndTime;
}
/**
* Removes and returns the next step from the unscheduled steps. If there are no
* steps left, then null is returned.
*
* @return the next step if there are any ready ones left, otherwise null
*/
public Step removeNextStep() {
if (this.isEmpty() || !this.isReady()) {
return null;
}
Step nextStep = this.steps.remove(0);
if (nextStep.isSimultaneous()) {
this.isReady = false;
this.simultaneousToEndTime -= nextStep.getDurationMinutes();
for (Step currStep : this.steps) {
if (currStep.isSimultaneous()) {
break;
}
this.simultaneousToEndTime -= currStep.getDurationMinutes();
}
}
return nextStep;
}
/**
* Returns true if the previously removed step has been completed
* and the next steps are ready.
*
* @return true if the next steps are ready to be done
*/
public boolean isReady() {
return this.isReady;
}
/**
* Sets the next steps as ready to be completed.
*/
public void setReady() {
this.isReady = true;
}
/**
* Returns if there are any unscheduled steps left.
*
* @return true if there are unscheduled steps left, false otherwise
*/
public boolean isEmpty() {
return this.steps.isEmpty();
}
}
/**
* A private helper class used to keep steps associated with
* their recipes.
*/
private class ScheduledStep {
public final Step step;
public final Recipe motherRecipe;
public ScheduledStep(@NonNull Step s, @NonNull Recipe r) {
this.step = s;
this.motherRecipe = r;
}
}
}