/*
* Copyright (C) Jakub Neubauer, 2007
*
* This file is part of TaskBlocks
*
* TaskBlocks 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.
*
* TaskBlocks 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package taskblocks.graph;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import javax.swing.event.ChangeListener;
import taskblocks.utils.ArrayUtils;
import taskblocks.utils.Utils;
/**
* helper class used to build the data structure from task graph model.
*/
public class TaskGraphRepresentation {
TaskRow[] _rows;
Task[] _tasks;
Connection[] _connections;
TaskModel _model;
/** Used internally when shifting tasks */
boolean _somethingMoved;
private ChangeListener _graphChangeListener;
/** Used to recognize when to recount bounds in {@link TaskGraphComponent#paint(java.awt.Graphics)}. */
private boolean _paintDirty;
/** Used to recognize if the project has been changed since loaded from file */
private boolean _saveDirty;
TaskGraphRepresentation(TaskModel model) {
_model = model;
}
public void clearSaveDirtyFlag() {
_saveDirty = false;
if(_graphChangeListener != null) {
_graphChangeListener.stateChanged(null);
}
}
public boolean isSaveDirty() {
return _saveDirty;
}
/**
* Builds all the data structures according to the model
* and recounts the starting times, so the tasks don't cross each other
*/
void buildFromModel() {
ChangeListener oldChangeList = _graphChangeListener;
try {
_graphChangeListener = null;
Object[] taskObjs = _model.getTasks();
Task[] tasks = new Task[taskObjs.length];
List<TaskRow> rows = new ArrayList<TaskRow>();
// build list of tasks and rows.
int i = 0;
for(Object taskObj: taskObjs) {
Task t = new Task(taskObj, this);
tasks[i] = t;
Object manObj = _model.getTaskMan(taskObj);
TaskRow row = findRowForManObj(manObj, rows);
if(row == null) {
row = new TaskRow(manObj);
row._name = _model.getManName(manObj);
row._workload = _model.getManWorkload(manObj);
rows.add(row);
row._index = rows.size()-1;
}
// task -> row mapping
t._row = row;
t.setEffort(_model.getTaskEffort(t._userObject), _model.getTaskWorkedTime(t._userObject));
t.setStartTime(_model.getTaskStartTime(t._userObject));
t.setComment( _model.getTaskComment(t._userObject) );
// we must initialize these arrays, so we don't care about nulls later
t._incommingConnections = new Connection[0];
t._outgoingConnections = new Connection[0];
i++;
}
// some rows doesn't have tasks, so create them
for(Object manObj: _model.getMans()) {
TaskRow row = findRowForManObj(manObj, rows);
if(row == null) {
row = new TaskRow(manObj);
row._name = _model.getManName(manObj);
row._workload = _model.getManWorkload(manObj);
rows.add(row);
row._index = rows.size()-1;
}
}
// row -> tasks mapping
for(TaskRow row: rows) {
List<Task> rowTasks = new ArrayList<Task>();
for(Task t: tasks) {
if(t.getRow() == row) {
rowTasks.add(t);
}
}
row._tasks = rowTasks.toArray(new Task[rowTasks.size()]);
}
// build connections array
List<Connection> connections = new ArrayList<Connection>();
for(Task t: tasks) {
Object[] predecessorsUserObjects = _model.getTaskPredecessors(t._userObject);
if(predecessorsUserObjects != null && predecessorsUserObjects.length > 0) {
for(Object predecessorUserObject: predecessorsUserObjects) {
Task predTask = findTaskForUserObject(predecessorUserObject, tasks);
Connection con = new Connection(predTask, t);
predTask.addOutgoingConnection(con);
t.addIncommingConnection(con);
connections.add(con);
}
}
}
synchronized(this) {
_tasks = tasks;
_rows = rows.toArray(new TaskRow[rows.size()]);
Arrays.sort(_rows, new TaskRowNameComparator());
// repair row indexes
for(i = 0; i < _rows.length; i++) {
_rows[i]._index = i;
}
_connections = connections.toArray(new Connection[connections.size()]);
recountStartingTimes();
_paintDirty = true;
_saveDirty = false;
}
} finally {
_graphChangeListener = oldChangeList;
if(_graphChangeListener != null) {
_graphChangeListener.stateChanged(null);
}
}
}
void changeTaskRow(Task t, TaskRow newRow) {
TaskRow oldRow = t._row;
if(oldRow == newRow) {
return;
}
t._row = newRow;
oldRow._tasks = (Task[])ArrayUtils.removeFromArray(oldRow._tasks, t);
newRow._tasks = (Task[])ArrayUtils.addToArray(newRow._tasks, t);
setDirty();
}
/**
* Must be called synchronized on this object
*/
void clearPaintDirtyFlag() {
_paintDirty = false;
}
ChangeListener getGraphChangeListener() {
return _graphChangeListener;
}
boolean isPaintDirty() {
return _paintDirty;
}
private List<Task> getRootTasks() {
// build a list of tasks that don't have predecessor.
List<Task> rootTasks = new ArrayList<Task>();
for(Task t: _tasks) {
if(t._incommingConnections.length == 0) {
rootTasks.add(t);
}
}
return rootTasks;
}
/**
* Shifts tasks as it is needed, so they don't cross each other and the dependencies are OK.
*/
synchronized void recountStartingTimes() {
TaskStartTimeComarator taskStartTimeComparator = new TaskStartTimeComarator();
// sort tasks according to their starting time
Arrays.sort(_tasks, taskStartTimeComparator);
for(TaskRow row: _rows) {
// first, we must sort them according to their starting time, so they will appear
// in the same order as before.
Arrays.sort(row._tasks, taskStartTimeComparator);
}
do {
_somethingMoved = false;
// build a list of tasks that don't have predecessor.
List<Task> rootTasks = getRootTasks();
// first, shift tasks according their predecessors.
for(Task t: rootTasks) {
shiftSubsequentTasksAfterMe(t);
}
// secondly, shift tasks, so they will not cross each other in row.
for(TaskRow row: _rows) {
// we must sort them according to their starting time again, the previous
// shifting could change their order.
Arrays.sort(row._tasks, taskStartTimeComparator);
for(int i = 0; i < row._tasks.length-1; i++) {
Task t1 = row._tasks[i];
Task t2 = row._tasks[i+1];
long t1End = t1.getFinishTime();
if(t2.getStartTime() < t1End) {
_somethingMoved = true;
// we must shift the t2 task. After shifting, check it's subsequent tasks.
t2.setStartTime(t1End);
shiftSubsequentTasksAfterMe(t2);
}
}
}
} while(_somethingMoved);
}
/**
* Sets both, the "paint" and the "save" dirty flags
*/
public synchronized void setDirty() {
_paintDirty = true;
_saveDirty = true;
if(_graphChangeListener != null) {
_graphChangeListener.stateChanged(null);
}
}
void setGraphChangeListener(ChangeListener cl) {
_graphChangeListener = cl;
}
/**
* Sets just the "paint" dirty flag.
* Used only when the tasks bounds should be recounted but the model itself didn't change
* (for example just scale changed)
*/
synchronized void setPaintDirty() {
_paintDirty = true;
}
public synchronized void shrinkTasks() {
TaskStartTimeComarator taskStartTimeComparator = new TaskStartTimeComarator();
// find the lowest time
long now = System.currentTimeMillis()/Utils.MILLISECONDS_PER_DAY;
long firstTime = Long.MAX_VALUE;
for(Task t: _tasks) {
firstTime = Math.min(t.getStartTime(), firstTime);
}
// repair the lowest time, to be not before "now"
firstTime = Math.max(now, firstTime);
for(TaskRow row: _rows) {
if(row._tasks.length > 0) {
// first, we must sort them according to their starting time, so they will appear
// in the same order as before.
Arrays.sort(row._tasks, taskStartTimeComparator);
// FIXED ISSUE #10 Shrink only tasks that are not yet started (ignore those before "now")
// and those that are moved are never moved before now.
for(int i = 0; i < row._tasks.length; i++) {
if(row._tasks[i].getStartTime() > firstTime) {
if(i == 0) {
row._tasks[i].setStartTime(firstTime);
} else {
row._tasks[i].setStartTime(Math.max(row._tasks[i-1].getFinishTime(), firstTime));
}
}
}
}
}
recountStartingTimes();
}
public void beginUpdateModelGroup(String groupName) {
_model.beginUpdateGroup(groupName);
}
public void endUpdateModelGroup() {
_model.endUpdateGroup();
}
public void updateModel() {
for(Task t: _tasks) {
// build array of preceeding tasks
Object[] preceedingTasksUserObjs = new Object[t._incommingConnections.length];
for(int i = 0; i < t._incommingConnections.length; i++) {
preceedingTasksUserObjs[i] = t._incommingConnections[i]._fromTask._userObject;
}
_model.updateTask(t._userObject, t._row._userManObject, t.getStartTime(), t.getEffort(), t.getActualDuration(), preceedingTasksUserObjs);
}
}
/**
* Used when building data structures. Finds row for given user man object
*
* @param manObj
* @param rows
* @return
*/
private TaskRow findRowForManObj(Object manObj, Collection<TaskRow> rows) {
for(TaskRow row:rows) {
if(row._userManObject == manObj) {
return row;
}
}
return null;
}
private Task findTaskForUserObject(Object taskUserobj, Task[] tasks) {
for(Task t: tasks) {
if(t._userObject == taskUserobj) {
return t;
}
}
return null;
}
/** Recursively checks all sub-tasks of given task and if they start before this task finishes,
* shifts them.
*
* @param t Task to be checked
*/
private void shiftSubsequentTasksAfterMe(Task t) {
for(Connection c: t._outgoingConnections) {
Task targetTask = c._toTask;
if(targetTask.getStartTime() < t.getFinishTime()) {
targetTask.setStartTime(t.getFinishTime());
_somethingMoved = true;
}
}
// recursion breath-first
for(Connection c: t._outgoingConnections) {
shiftSubsequentTasksAfterMe(c._toTask);
}
}
/**
*
* @param t1
* @param t2
*
* @throws Exception if connection cannot be created because of cycle.
*/
void createConnection(Task t1, Task t2) throws Exception {
if(t1 == t2 || t1 == null || t2 == null) {
throw new IllegalArgumentException("Wrong connection");
}
Connection c = new Connection(t1, t2);
t1._outgoingConnections = (Connection[])ArrayUtils.addToArray(t1._outgoingConnections, c);
t2._incommingConnections = (Connection[])ArrayUtils.addToArray(t2._incommingConnections, c);
_connections = (Connection[])ArrayUtils.addToArray(_connections, c);
if(checkForCycles()) {
// UNDO and throw exception
t1._outgoingConnections = (Connection[])ArrayUtils.removeFromArray(t1._outgoingConnections, c);
t2._incommingConnections = (Connection[])ArrayUtils.removeFromArray(t2._incommingConnections, c);
_connections = (Connection[])ArrayUtils.removeFromArray(_connections, c);
throw new Exception("Loops are not allowed in task dependencies");
}
setDirty();
}
/**
* Returns true if there are some cycles in the task dependency graph
*
* @return
*/
private boolean checkForCycles() {
if(_tasks.length == 0) {
return false;
}
// initialize flags
for(Task t: _tasks) {
t._flag = false;
}
for(Task t: _tasks) {
if(checkForCyclesRec(t)) {
return true;
}
}
return false;
}
private boolean checkForCyclesRec(Task t) {
if(t._flag) {
return true;
}
t._flag = true;
for(Connection outC: t._outgoingConnections) {
if(checkForCyclesRec(outC._toTask)) {
return true;
}
}
t._flag = false;
return false;
}
public int getManCount() {
return _rows.length;
}
public void removeConnection(Connection c) {
c._fromTask._outgoingConnections = (Connection[])ArrayUtils.removeFromArray(c._fromTask._outgoingConnections, c);
c._toTask._incommingConnections = (Connection[])ArrayUtils.removeFromArray(c._toTask._incommingConnections, c);
_connections = (Connection[])ArrayUtils.removeFromArray(_connections, c);
setDirty();
}
public void removeTask(Task t) {
for(Connection c: t._incommingConnections) {
removeConnection(c);
}
for(Connection c: t._outgoingConnections) {
removeConnection(c);
}
_tasks = (Task[])ArrayUtils.removeFromArray(_tasks, t);
t._row._tasks = (Task[])ArrayUtils.removeFromArray(t._row._tasks, t);
_model.removeTask(t._userObject);
setDirty();
}
public void removeRow(TaskRow row) {
for(Task t: row._tasks) {
removeTask(t);
}
_rows = (TaskRow[])ArrayUtils.removeFromArray(_rows, row);
_model.removeMan(row._userManObject);
setDirty();
}
}