/*
* This file is part of LibrePlan
*
* Copyright (C) 2009-2010 Fundación para o Fomento da Calidade Industrial e
* Desenvolvemento Tecnolóxico de Galicia
* Copyright (C) 2010-2012 Igalia, S.L.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.zkoss.ganttz;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import org.apache.commons.lang3.Validate;
import org.joda.time.DateTime;
import org.joda.time.Days;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.zkoss.ganttz.adapters.IDisabilityConfiguration;
import org.zkoss.ganttz.data.GanttDate;
import org.zkoss.ganttz.data.ITaskFundamentalProperties.IModifications;
import org.zkoss.ganttz.data.ITaskFundamentalProperties.IUpdatablePosition;
import org.zkoss.ganttz.data.Milestone;
import org.zkoss.ganttz.data.Task;
import org.zkoss.ganttz.data.Task.IReloadResourcesTextRequested;
import org.zkoss.ganttz.data.constraint.Constraint;
import org.zkoss.ganttz.data.constraint.Constraint.IConstraintViolationListener;
import org.zkoss.ganttz.util.WeakReferencedListeners.Mode;
import org.zkoss.lang.Objects;
import org.zkoss.zk.au.AuRequest;
import org.zkoss.zk.au.AuService;
import org.zkoss.zk.au.out.AuInvoke;
import org.zkoss.zk.mesg.MZk;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Event;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.AfterCompose;
import org.zkoss.zk.ui.sys.ContentRenderer;
import org.zkoss.zul.Div;
/**
* Graphical component which represents a {@link Task}.
*
* @author Javier Morán Rúa <jmoran@igalia.com>
* @author Manuel Rego Casasnovas <rego@igalia.com>
*/
public class TaskComponent extends Div implements AfterCompose {
private static final int HEIGHT_PER_TASK = 10;
private static final int CONSOLIDATED_MARK_HALF_WIDTH = 3;
private static final int HALF_DEADLINE_MARK = 3;
private String FUNCTION_RESIZE = "resizeCompletion2Advance";
protected final IDisabilityConfiguration disabilityConfiguration;
private PropertyChangeListener criticalPathPropertyListener;
private PropertyChangeListener showingAdvancePropertyListener;
private PropertyChangeListener showingReportedHoursPropertyListener;
private PropertyChangeListener showingMoneyCostBarPropertyListener;
private IReloadResourcesTextRequested reloadResourcesTextRequested;
private String _color;
private boolean isTopLevel;
private final Task task;
private transient PropertyChangeListener propertiesListener;
private String progressType;
public static TaskComponent asTaskComponent(Task task,
IDisabilityConfiguration disabilityConfiguration,
boolean isTopLevel) {
final TaskComponent result;
if ( task.isContainer() ) {
result = TaskContainerComponent.asTask(task, disabilityConfiguration);
} else if ( task instanceof Milestone ) {
result = new MilestoneComponent(task, disabilityConfiguration);
} else {
result = new TaskComponent(task, disabilityConfiguration);
}
result.isTopLevel = isTopLevel;
return TaskRow.wrapInRow(result);
}
public static TaskComponent asTaskComponent(Task task, IDisabilityConfiguration disabilityConfiguration) {
return asTaskComponent(task, disabilityConfiguration, true);
}
public TaskComponent(Task task, IDisabilityConfiguration disabilityConfiguration) {
setHeight(HEIGHT_PER_TASK + "px");
setContext("idContextMenuTaskAssignment");
this.task = task;
setClass(calculateCSSClass());
setId(UUID.randomUUID().toString());
this.disabilityConfiguration = disabilityConfiguration;
IConstraintViolationListener<GanttDate> taskViolationListener = Constraint.onlyOnZKExecution(new IConstraintViolationListener<GanttDate>() {
@Override
public void constraintViolated(Constraint<GanttDate> constraint, GanttDate value) {
// TODO mark graphically task as violated
}
@Override
public void constraintSatisfied(Constraint<GanttDate> constraint, GanttDate value) {
// TODO mark graphically dependency as not violated
}
});
this.task.addConstraintViolationListener(taskViolationListener, Mode.RECEIVE_PENDING);
reloadResourcesTextRequested = new IReloadResourcesTextRequested() {
@Override
public void reloadResourcesTextRequested() {
if ( canShowResourcesText() ) {
smartUpdate("resourcesText", getResourcesText());
}
String cssClass = calculateCSSClass();
response("setClass", new AuInvoke(TaskComponent.this, "setClass", cssClass));
// FIXME: Refactor to another listener
updateDeadline();
invalidate();
}
};
this.task.addReloadListener(reloadResourcesTextRequested);
setAuService(new AuService() {
public boolean service(AuRequest request, boolean everError) {
String command = request.getCommand();
final TaskComponent ta;
if ( command.equals("onUpdatePosition") ){
ta = retrieveTaskComponent(request);
ta.doUpdatePosition(toInteger(retrieveData(request, "left"))
);
Events.postEvent(new Event(getId(), ta, request.getData()));
return true;
}
if ( command.equals("onUpdateWidth") ){
ta = retrieveTaskComponent(request);
ta.doUpdateSize(toInteger(retrieveData(request, "width")));
Events.postEvent(new Event(getId(), ta, request.getData()));
return true;
}
if ( command.equals("onAddDependency") ){
ta = retrieveTaskComponent(request);
ta.doAddDependency((String) retrieveData(request, "dependencyId"));
Events.postEvent(new Event(getId(), ta, request.getData()));
return true;
}
return false;
}
private int toInteger(Object valueFromRequestData) {
return ((Number) valueFromRequestData).intValue();
}
private TaskComponent retrieveTaskComponent(AuRequest request) {
final TaskComponent ta = (TaskComponent) request.getComponent();
if ( ta == null ) {
throw new UiException(MZk.ILLEGAL_REQUEST_COMPONENT_REQUIRED, this);
}
return ta;
}
private Object retrieveData(AuRequest request, String key) {
Object value = request.getData().get(key);
if ( value == null )
throw new UiException(MZk.ILLEGAL_REQUEST_WRONG_DATA, new Object[] { key, this });
return value;
}
});
}
/* Generate CSS class attribute depending on task properties */
protected String calculateCSSClass() {
String cssClass = isSubcontracted() ? "box subcontracted-task" : "box standard-task";
cssClass += isResizingTasksEnabled() ? " yui-resize" : "";
if ( isContainer() ) {
cssClass += task.isExpanded() ? " expanded" : " closed ";
cssClass += task.isInCriticalPath() && !task.isExpanded() ? " critical" : "";
} else {
cssClass += task.isInCriticalPath() ? " critical" : "";
if ( !task.canBeExplicitlyMoved() ) {
cssClass += " fixed";
}
}
cssClass += " " + task.getAssignedStatus();
if ( task.isLimiting() ) {
cssClass += task.isLimitingAndHasDayAssignments() ? " limiting-assigned " : " limiting-unassigned ";
}
if ( task.isRoot() && task.belongsClosedProject() ) {
cssClass += " project-closed ";
}
return cssClass;
}
public boolean isLimiting() {
return task.isLimiting();
}
protected void updateClass() {
setSclass(calculateCSSClass());
}
public final void afterCompose() {
updateProperties();
if ( propertiesListener == null ) {
propertiesListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateProperties();
}
};
}
this.task.addFundamentalPropertiesChangeListener(propertiesListener);
if ( showingAdvancePropertyListener == null ) {
showingAdvancePropertyListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ( isInPage() && !(task instanceof Milestone) ) {
updateCompletionAdvance();
}
}
};
}
this.task.addAdvancesPropertyChangeListener(showingAdvancePropertyListener);
if ( showingReportedHoursPropertyListener == null ) {
showingReportedHoursPropertyListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ( isInPage() && !(task instanceof Milestone) ) {
updateCompletionReportedHours();
}
}
};
}
this.task.addReportedHoursPropertyChangeListener(showingReportedHoursPropertyListener);
if ( showingMoneyCostBarPropertyListener == null ) {
showingMoneyCostBarPropertyListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if ( isInPage() && !(task instanceof Milestone) ) {
updateCompletionMoneyCostBar();
}
}
};
}
this.task.addMoneyCostBarPropertyChangeListener(showingMoneyCostBarPropertyListener);
if ( criticalPathPropertyListener == null ) {
criticalPathPropertyListener = new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateClass();
}
};
}
this.task.addCriticalPathPropertyChangeListener(criticalPathPropertyListener);
updateClass();
}
/**
* Note: This method is intended to be overridden.
*/
protected boolean canShowResourcesText() {
return true;
}
public TaskRow getRow() {
if ( getParent() == null ) {
throw new IllegalStateException(
"the TaskComponent should have been wraped by a " + TaskRow.class.getName());
}
return (TaskRow) getParent();
}
public Task getTask() {
return task;
}
public String getTaskName() {
return task.getName();
}
public String getLength() {
return null;
}
public boolean isResizingTasksEnabled() {
return (disabilityConfiguration != null) &&
disabilityConfiguration.isResizingTasksEnabled() &&
!task.isSubcontracted() && task.canBeExplicitlyResized();
}
public boolean isMovingTasksEnabled() {
return (disabilityConfiguration != null) &&
disabilityConfiguration.isMovingTasksEnabled() &&
task.canBeExplicitlyMoved();
}
void doUpdatePosition(int leftX) {
GanttDate startBeforeMoving = this.task.getBeginDate();
final LocalDate newPosition = getMapper().toDate(leftX);
this.task.doPositionModifications(new IModifications() {
@Override
public void doIt(IUpdatablePosition position) {
position.moveTo(GanttDate.createFrom(newPosition));
}
});
boolean remainsInOriginalPosition = this.task.getBeginDate().equals(startBeforeMoving);
if ( remainsInOriginalPosition ) {
updateProperties();
}
}
void doUpdateSize(int size) {
DateTime end =
new DateTime(this.task.getBeginDate().toDayRoundedDate().getTime()).plus(getMapper().toDuration(size));
this.task.resizeTo(end.toLocalDate());
updateProperties();
}
void doAddDependency(String destinyTaskId) {
getTaskList().addDependency(this, ((TaskComponent) getFellow(destinyTaskId)));
}
public String getColor() {
return _color;
}
public void setColor(String color) {
if ( (color != null) && (color.length() == 0) ) {
color = null;
}
if ( !Objects.equals(_color, color) ) {
_color = color;
}
}
/* We override the method of renderProperties to put the color property as part of the style */
protected void renderProperties(ContentRenderer renderer) throws IOException{
if ( getColor() != null )
setStyle("background-color : " + getColor());
setWidgetAttribute("movingTasksEnabled", ((Boolean)isMovingTasksEnabled()).toString());
setWidgetAttribute("resizingTasksEnabled", ((Boolean)isResizingTasksEnabled()).toString());
/* We can't use setStyle because of restrictions involved with UiVisualizer#getResponses and the
* smartUpdate method (when the request is asynchronous)
*/
render(renderer, "style", "position : absolute");
render(renderer, "_labelsText", getLabelsText());
render(renderer, "_resourcesText", getResourcesText());
render(renderer, "_tooltipText", getTooltipText());
super.renderProperties(renderer);
}
/* We send a response to the client to create the arrow we are going to use to create the dependency */
public void addDependency() {
response("depkey", new AuInvoke(this, "addDependency"));
}
private IDatesMapper getMapper() {
return getTaskList().getMapper();
}
public TaskList getTaskList() {
return getRow().getTaskList();
}
@Override
public void setParent(Component parent) {
Validate.isTrue(parent == null || parent instanceof TaskRow);
super.setParent(parent);
}
public final void zoomChanged() {
updateProperties();
}
public void updateProperties() {
if ( !isInPage() ) {
return;
}
setLeft("0");
setLeft(this.task.getBeginDate().toPixels(getMapper()) + "px");
updateWidth();
smartUpdate("name", this.task.getName());
updateDeadline();
updateCompletionIfPossible();
updateClass();
}
private void updateWidth() {
setWidth("0");
int pixelsEnd = this.task.getEndDate().toPixels(getMapper());
int pixelsStart = this.task.getBeginDate().toPixels(getMapper());
setWidth((pixelsEnd - pixelsStart) + "px");
}
private void updateDeadline() {
// Task mark is placed after midnight date of the deadline day
if ( task.getDeadline() != null ) {
String position = (getMapper().toPixels(
LocalDate.fromDateFields(task.getDeadline()).plusDays(1)) - HALF_DEADLINE_MARK) + "px";
response(null, new AuInvoke(this, "moveDeadline", position));
} else {
// Move deadline out of visible area
response(null, new AuInvoke(this, "moveDeadline","-100px"));
}
if ( task.getConsolidatedline() != null ) {
int pixels = getMapper()
.toPixels(LocalDate.fromDateFields(task.getConsolidatedline().toDayRoundedDate()))
- CONSOLIDATED_MARK_HALF_WIDTH;
String position = pixels + "px";
response(null, new AuInvoke(this, "moveConsolidatedline", position));
} else {
// Move consolidated line out of visible area
response(null, new AuInvoke(this, "moveConsolidatedline", "-100px"));
}
}
public void updateCompletionIfPossible() {
if ( task instanceof Milestone ) {
return;
}
updateCompletionReportedHours();
updateCompletionMoneyCostBar();
updateCompletionAdvance();
}
public void updateCompletionReportedHours() {
if ( task.isShowingReportedHours() ) {
int startPixels = this.task.getBeginDate().toPixels(getMapper());
String widthHoursAdvancePercentage =
pixelsFromStartUntil(startPixels, this.task.getHoursAdvanceBarEndDate()) + "px";
response(null, new AuInvoke(this, "resizeCompletionAdvance", widthHoursAdvancePercentage));
Date firstTimesheetDate = task.getFirstTimesheetDate();
Date lastTimesheetDate = task.getLastTimesheetDate();
if ( firstTimesheetDate != null && lastTimesheetDate != null ) {
Duration firstDuration = Days.daysBetween(
task.getBeginDateAsLocalDate(),
LocalDate.fromDateFields(firstTimesheetDate)).toStandardDuration();
int pixelsFirst = getMapper().toPixels(firstDuration);
String positionFirst = pixelsFirst + "px";
Duration lastDuration = Days.daysBetween(
task.getBeginDateAsLocalDate(),
LocalDate.fromDateFields(lastTimesheetDate).plusDays(1)).toStandardDuration();
int pixelsLast = getMapper().toPixels(lastDuration);
String positionLast = pixelsLast + "px";
response(null, new AuInvoke(this, "showTimsheetDateMarks", positionFirst, positionLast));
} else {
response(null, new AuInvoke(this, "hideTimsheetDateMarks"));
}
} else {
response(null, new AuInvoke(this, "resizeCompletionAdvance", "0px"));
response(null, new AuInvoke(this, "hideTimsheetDateMarks"));
}
}
public void updateCompletionMoneyCostBar() {
if ( task.isShowingMoneyCostBar() ) {
int startPixels = this.task.getBeginDate().toPixels(getMapper());
int endPixels = this.task.getEndDate().toPixels(getMapper());
int widthPixels = (int) ((endPixels - startPixels) * this.task.getMoneyCostBarPercentage().doubleValue());
String widthMoneyCostBar = widthPixels + "px";
response(null, new AuInvoke(this, "resizeCompletionMoneyCostBar", widthMoneyCostBar));
} else {
response(null, new AuInvoke(this, "resizeCompletionMoneyCostBar", "0px"));
}
}
private void updateCompletionAdvance() {
if ( task.isShowingAdvances() ) {
int startPixels = this.task.getBeginDate().toPixels(getMapper());
String widthAdvancePercentage = pixelsFromStartUntil(startPixels, this.task.getAdvanceBarEndDate()) + "px";
response(null, new AuInvoke(this, FUNCTION_RESIZE, widthAdvancePercentage));
} else {
response(null, new AuInvoke(this, FUNCTION_RESIZE, "0px"));
}
}
public void updateCompletion(String progressType) {
if ( task.isShowingAdvances() ) {
int startPixels = this.task.getBeginDate().toPixels(getMapper());
String widthAdvancePercentage =
pixelsFromStartUntil(startPixels, this.task.getAdvanceBarEndDate(progressType)) + "px";
response(null, new AuInvoke(this, FUNCTION_RESIZE, widthAdvancePercentage));
} else {
response(null, new AuInvoke(this, FUNCTION_RESIZE, "0px"));
}
}
private int pixelsFromStartUntil(int startPixels, GanttDate until) {
int endPixels = until.toPixels(getMapper());
assert endPixels >= startPixels;
return endPixels - startPixels;
}
public void updateTooltipText() {
this.progressType = null;
}
public void updateTooltipText(String progressType) {
this.progressType = progressType;
}
private boolean isInPage() {
return getPage() != null;
}
void publishTaskComponents(Map<Task, TaskComponent> resultAccumulated) {
resultAccumulated.put(getTask(), this);
publishDescendants(resultAccumulated);
}
protected void publishDescendants(Map<Task, TaskComponent> resultAccumulated) {
}
protected void remove() {
this.getRow().detach();
task.removeReloadListener(reloadResourcesTextRequested);
}
public boolean isTopLevel() {
return isTopLevel;
}
public String getTooltipText() {
if ( progressType == null ) {
return task.getTooltipText();
} else {
return task.updateTooltipText(progressType);
}
}
public String getLabelsText() {
return task.getLabelsText();
}
public String getResourcesText() {
return task.getResourcesText();
}
public boolean isSubcontracted() {
return task.isSubcontracted();
}
public boolean isContainer() {
return task.isContainer();
}
@Override
public String toString() {
return task.toString();
}
}