package com.lassekoskela.maven.timeline;
import java.util.Comparator;
import java.util.LinkedList;
import org.codehaus.plexus.logging.Logger;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.collect.ImmutableSortedSet.Builder;
import com.google.common.collect.Lists;
import com.google.common.collect.TreeMultiset;
import com.google.common.primitives.Ints;
import com.google.common.primitives.Longs;
import com.lassekoskela.maven.bean.Goal;
import com.lassekoskela.maven.bean.Phase;
import com.lassekoskela.maven.bean.Project;
import com.lassekoskela.maven.bean.Timeline;
public class GoalOrganizer {
public static final int COLUMN_WIDTH_PIXEL = 60;
public static final int HEIGHT_SECOND_SCALING_PIXEL = 4;
public static final double HEIGHT_MSECOND_SCALING_PIXEL = HEIGHT_SECOND_SCALING_PIXEL / 1000d;
private final Logger logger;
public GoalOrganizer(Logger logger) {
this.logger = logger;
}
public Iterable<DisplayableGoal> organize(Timeline timeline) {
ImmutableList.Builder<DisplayableGoal> goals = ImmutableList.builder();
Columns columnOfGoals = new Columns();
Iterable<SortedGoal> sortedGoalsByStartTime = sortedGoalsByStartTime(timeline);
logGoals(sortedGoalsByStartTime);
for (SortedGoal sortedGoal : sortedGoalsByStartTime) {
goals.add(buildDisplayableGoal(columnOfGoals, sortedGoal));
}
return goals.build();
}
private void logGoals(Iterable<SortedGoal> sortedGoalsByStartTime) {
if (logger.isDebugEnabled()) {
String formatColumns = "%-70s%15s%15s%15s";
logger.debug(String.format(formatColumns, "Goal", "start time", "duration", "end time"));
for (SortedGoal sortedGoal : sortedGoalsByStartTime) {
Goal goal = sortedGoal.goal;
logger.debug(String.format(formatColumns,
sortedGoal.project.getItemId() + " " + sortedGoal.phase.getItemId() + " " + goal.getItemId(),
goal.getStartTimeInMs(), goal.getDuration().toString(), goal.getCompletedTimeInMs()));
}
}
}
@VisibleForTesting
Iterable<SortedGoal> sortedGoalsByStartTime(Timeline timeline) {
Builder<SortedGoal> sortingGoalSet = ImmutableSortedSet.orderedBy(new GoalStartTimeComparator());
for (Project project : timeline.getProjects()) {
for (Phase phase : project.getPhases()) {
for (Goal goal : phase.getGoals()) {
sortingGoalSet.add(new SortedGoal(project, phase, goal));
}
}
}
return sortingGoalSet.build();
}
@VisibleForTesting
Comparator<SortedGoal> newGoalStartTimeComparator() {
return new GoalStartTimeComparator();
}
@VisibleForTesting
DisplayableGoal buildDisplayableGoal(Columns columnOfGoals, SortedGoal sortedGoal) {
return new DisplayableGoal(sortedGoal.project.getItemId(), sortedGoal.phase.getItemId(),
sortedGoal.goal.getItemId(), sortedGoal.goal.serializeDependencies(),
leftPositionForColumn(appendGoalInRightColumn(columnOfGoals, sortedGoal.goal)),
goalTopPosition(sortedGoal.goal), goalHeightValue(sortedGoal.goal));
}
private int goalHeightValue(Goal goal) {
return topPositionForDuration(goal.getDuration().inMillis());
}
private int goalTopPosition(Goal goal) {
return topPositionForDuration(goal.getStartTimeInMs());
}
@VisibleForTesting
int topPositionForDuration(long millis) {
Preconditions.checkArgument(millis >= 0);
return (int) Math.round(Math.ceil(millis * HEIGHT_MSECOND_SCALING_PIXEL));
}
@VisibleForTesting
int leftPositionForColumn(int goalColumn) {
Preconditions.checkArgument(goalColumn >= 0);
return goalColumn * COLUMN_WIDTH_PIXEL;
}
private int appendGoalInRightColumn(Columns columnOfGoals, Goal goal) {
return columnOfGoals.addGoal(goal);
}
@VisibleForTesting
static class Column implements Comparable<Column> {
private final int index;
private final LinkedList<Goal> goals;
@VisibleForTesting
Column(int index) {
this.index = index;
goals = Lists.newLinkedList();
}
public void addGoal(Goal goal) {
goals.add(goal);
}
public long lastGoalEnd() {
if (goals.isEmpty()) {
return 0;
}
return goals.getLast().getCompletedTimeInMs();
}
@Override
public int compareTo(Column o) {
int comparingGoalEnd = Longs.compare(lastGoalEnd(), o.lastGoalEnd());
if (comparingGoalEnd == 0) {
return Ints.compare(this.index, o.index);
}
return comparingGoalEnd;
}
public boolean goalFits(Goal goal) {
return goal.getStartTimeInMs() >= lastGoalEnd();
}
}
@VisibleForTesting
static class Columns {
private TreeMultiset<Column> columns;
@VisibleForTesting
Columns() {
columns = TreeMultiset.create();
columns.add(newColumn());
}
private Column newColumn() {
return new Column(columns.size());
}
public int addGoal(Goal goal) {
Column column = selectColumn(goal);
addGoalToColumn(goal, column);
return column.index;
}
private Column selectColumn(Goal goal) {
Column earliestEndingColumn = columns.firstEntry().getElement();
if (earliestEndingColumn.goalFits(goal)) {
return columns.pollFirstEntry().getElement();
}
return newColumn();
}
private void addGoalToColumn(Goal goal, Column column) {
column.addGoal(goal);
columns.add(column);
}
}
@VisibleForTesting
static class SortedGoal {
private final Project project;
private final Phase phase;
final Goal goal;
@VisibleForTesting
SortedGoal(Project project, Phase phase, Goal goal) {
this.project = project;
this.phase = phase;
this.goal = goal;
}
@Override
public int hashCode() {
return Objects.hashCode(project, phase, goal);
}
@Override
public boolean equals(Object object) {
if (object instanceof SortedGoal) {
SortedGoal that = (SortedGoal) object;
return Objects.equal(this.project, that.project) && Objects.equal(this.phase, that.phase)
&& Objects.equal(this.goal, that.goal);
}
return false;
}
@Override
public String toString() {
return Objects.toStringHelper(this).add("project", project).add("phase", phase).add("goal", goal)
.toString();
}
}
public static class DisplayableGoal {
private final String projectId;
private final String phaseId;
private final String goalId;
private final String dependencies;
private final long leftPosition;
private final long topPosition;
private final long heightPosition;
@VisibleForTesting
DisplayableGoal(String projectId, String phaseId, String goalId, String dependencies, long leftPosition,
long topPosition, long heightPosition) {
this.projectId = projectId;
this.phaseId = phaseId;
this.goalId = goalId;
this.dependencies = dependencies;
this.leftPosition = leftPosition;
this.topPosition = topPosition;
this.heightPosition = heightPosition;
}
public String getProjectId() {
return projectId;
}
public String getPhaseId() {
return phaseId;
}
public String getGoalId() {
return goalId;
}
public String getDependencies() {
return dependencies;
}
public long getLeftPosition() {
return leftPosition;
}
public long getTopPosition() {
return topPosition;
}
public long getHeightPosition() {
return heightPosition;
}
@Override
public int hashCode() {
return Objects
.hashCode(projectId, phaseId, goalId, dependencies, leftPosition, topPosition, heightPosition);
}
@Override
public boolean equals(Object object) {
if (object instanceof DisplayableGoal) {
DisplayableGoal that = (DisplayableGoal) object;
return Objects.equal(this.projectId, that.projectId) && Objects.equal(this.phaseId, that.phaseId)
&& Objects.equal(this.goalId, that.goalId)
&& Objects.equal(this.dependencies, that.dependencies)
&& Objects.equal(this.leftPosition, that.leftPosition)
&& Objects.equal(this.topPosition, that.topPosition)
&& Objects.equal(this.heightPosition, that.heightPosition);
}
return false;
}
@Override
public String toString() {
return Objects.toStringHelper(this).add("projectId", projectId).add("phaseId", phaseId)
.add("goalId", goalId).add("dependencies", dependencies).add("leftPosition", leftPosition)
.add("topPosition", topPosition).add("heightPosition", heightPosition).toString();
}
}
}