/*
* 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-2011 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.libreplan.web.limitingresources;
import static org.libreplan.web.I18nHelper._;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import org.joda.time.Duration;
import org.joda.time.LocalDate;
import org.libreplan.business.common.exceptions.ValidationException;
import org.libreplan.business.orders.entities.OrderElement;
import org.libreplan.business.planner.entities.DayAssignment;
import org.libreplan.business.planner.entities.GenericResourceAllocation;
import org.libreplan.business.planner.entities.ResourceAllocation;
import org.libreplan.business.planner.entities.SpecificResourceAllocation;
import org.libreplan.business.planner.entities.Task;
import org.libreplan.business.planner.limiting.entities.DateAndHour;
import org.libreplan.business.planner.limiting.entities.LimitingResourceQueueElement;
import org.libreplan.business.resources.entities.Criterion;
import org.libreplan.business.resources.entities.LimitingResourceQueue;
import org.libreplan.business.workingday.IntraDayDate.PartialDay;
import org.zkoss.ganttz.DatesMapperOnInterval;
import org.zkoss.ganttz.IDatesMapper;
import org.zkoss.ganttz.timetracker.TimeTracker;
import org.zkoss.ganttz.timetracker.zoom.IZoomLevelChangedListener;
import org.zkoss.ganttz.timetracker.zoom.ZoomLevel;
import org.zkoss.ganttz.util.MenuBuilder;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.ext.AfterCompose;
import org.zkoss.zk.ui.sys.ContentRenderer;
import org.zkoss.zul.Div;
import org.zkoss.zul.impl.XulElement;
/**
* This class wraps ResourceLoad data inside an specific HTML Div component.
*
* @author Lorenzo Tilve Álvaro <ltilve@igalia.com>
* @author Vova Perebykivskyi <vova@libreplan-enterprise.com>
*/
public class QueueComponent extends XulElement implements AfterCompose {
private static final int DEADLINE_MARK_HALF_WIDTH = 5;
private final QueueListComponent queueListComponent;
private final TimeTracker timeTracker;
private transient IZoomLevelChangedListener zoomChangedListener;
private LimitingResourceQueue limitingResourceQueue;
private List<QueueTask> queueTasks = new ArrayList<>();
private QueueComponent(final QueueListComponent queueListComponent,
final TimeTracker timeTracker,
final LimitingResourceQueue limitingResourceQueue) {
this.queueListComponent = queueListComponent;
this.limitingResourceQueue = limitingResourceQueue;
this.timeTracker = timeTracker;
createChildren(limitingResourceQueue, timeTracker.getMapper());
/* Do not replace it with lamda */
zoomChangedListener = new IZoomLevelChangedListener() {
@Override
public void zoomLevelChanged(ZoomLevel detailLevel) {
getChildren().clear();
createChildren(limitingResourceQueue, timeTracker.getMapper());
}
};
this.timeTracker.addZoomListener(zoomChangedListener);
}
@Override
public void afterCompose() {
appendContextMenus();
}
public static QueueComponent create(QueueListComponent queueListComponent,
TimeTracker timeTracker,
LimitingResourceQueue limitingResourceQueue) {
return new QueueComponent(queueListComponent, timeTracker, limitingResourceQueue);
}
public List<QueueTask> getQueueTasks() {
return queueTasks;
}
public void setLimitingResourceQueue(LimitingResourceQueue limitingResourceQueue) {
this.limitingResourceQueue = limitingResourceQueue;
}
private void createChildren(LimitingResourceQueue limitingResourceQueue, IDatesMapper mapper) {
List<QueueTask> queueTasks = createQueueTasks(mapper, limitingResourceQueue.getLimitingResourceQueueElements());
appendQueueTasks(queueTasks);
}
public QueueListComponent getQueueListComponent() {
return queueListComponent;
}
public LimitingResourcesPanel getLimitingResourcesPanel() {
return queueListComponent.getLimitingResourcePanel();
}
public void invalidate() {
removeChildren();
appendQueueElements(limitingResourceQueue.getLimitingResourceQueueElements());
}
private void removeChildren() {
for (QueueTask each: queueTasks) {
removeChild(each);
}
queueTasks.clear();
}
private void appendQueueTasks(List<QueueTask> queueTasks) {
for (QueueTask each: queueTasks) {
appendQueueTask(each);
}
}
private void appendQueueTask(QueueTask queueTask) {
queueTasks.add(queueTask);
/*
* In this case after we migrated from ZK5 to ZK8, ZK was appending div to QueueComponent,
* on every allocation it was creating new QueueComponents, but DOM tree was still the same.
*/
getLimitingResourcesPanel().getFellow("insertionPointRightPanel").invalidate();
appendChild(queueTask);
}
private void removeQueueTask(QueueTask queueTask) {
queueTasks.remove(queueTask);
removeChild(queueTask);
}
private List<QueueTask> createQueueTasks(IDatesMapper datesMapper, Set<LimitingResourceQueueElement> list) {
List<QueueTask> result = new ArrayList<>();
org.zkoss.ganttz.util.Interval interval = null;
if ( timeTracker.getFilter() != null ) {
timeTracker.getFilter().resetInterval();
interval = timeTracker.getFilter().getCurrentPaginationInterval();
}
for (LimitingResourceQueueElement each : list) {
if ( interval != null ) {
if ( each.getEndDate().toDateTimeAtStartOfDay().isAfter(interval.getStart().toDateTimeAtStartOfDay()) &&
each.getStartDate().toDateTimeAtStartOfDay()
.isBefore(interval.getFinish().toDateTimeAtStartOfDay()) ) {
result.add(createQueueTask(datesMapper, each));
}
} else {
result.add(createQueueTask(datesMapper, each));
}
}
return result;
}
private static QueueTask createQueueTask(IDatesMapper datesMapper, LimitingResourceQueueElement element) {
validateQueueElement(element);
return createDivForElement(datesMapper, element);
}
private static OrderElement getRootOrder(Task task) {
OrderElement order = task.getOrderElement();
while (order.getParent() != null) {
order = order.getParent();
}
return order;
}
private static String createTooltiptext(LimitingResourceQueueElement element) {
final Task task = element.getResourceAllocation().getTask();
final OrderElement order = getRootOrder(task);
StringBuilder result = new StringBuilder();
result.append(_("Project: {0}", order.getName())).append(" ");
result.append(_("Task: {0}", task.getName())).append(" ");
result.append(_("Completed: {0}%", element.getAdvancePercentage().multiply(new BigDecimal(100)))).append(" ");
final ResourceAllocation<?> resourceAllocation = element.getResourceAllocation();
if ( resourceAllocation instanceof SpecificResourceAllocation ) {
final SpecificResourceAllocation specific = (SpecificResourceAllocation) resourceAllocation;
result.append(_("Resource: {0}", specific.getResource().getName())).append(" ");
} else if ( resourceAllocation instanceof GenericResourceAllocation ) {
final GenericResourceAllocation generic = (GenericResourceAllocation) resourceAllocation;
/* TODO resolve deprecated */
result.append(_("Criteria: {0}", Criterion.getCaptionFor(generic.getCriterions()))).append(" ");
}
result.append(_("Allocation: [{0},{1}]", element.getStartDate().toString(), element.getEndDate()));
return result.toString();
}
/**
* Returns end date considering % of task completion.
*
* @param element
* @return {@link DateAndHour}
*/
private static DateAndHour getAdvanceEndDate(LimitingResourceQueueElement element) {
int hoursWorked = 0;
final List<? extends DayAssignment> dayAssignments = element.getDayAssignments();
if ( element.hasDayAssignments() ) {
final int estimatedWorkedHours =
estimatedWorkedHours(element.getIntentedTotalHours(), element.getAdvancePercentage());
for (DayAssignment each: dayAssignments) {
hoursWorked += each.getDuration().getHours();
if ( hoursWorked >= estimatedWorkedHours ) {
int hourSlot = each.getDuration().getHours() - (hoursWorked - estimatedWorkedHours);
return new DateAndHour(each.getDay(), hourSlot);
}
}
}
if ( hoursWorked != 0 ) {
DayAssignment lastDayAssignment = dayAssignments.get(dayAssignments.size() - 1);
return new DateAndHour(lastDayAssignment.getDay(), lastDayAssignment.getDuration().getHours());
}
return null;
}
private static int estimatedWorkedHours(Integer totalHours, BigDecimal percentageWorked) {
boolean condition = totalHours != null && percentageWorked != null;
return condition ? percentageWorked.multiply(new BigDecimal(totalHours)).intValue() : 0;
}
private static QueueTask createDivForElement(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
final Task task = queueElement.getResourceAllocation().getTask();
final OrderElement order = getRootOrder(task);
QueueTask result = new QueueTask(queueElement);
String cssClass = "queue-element";
result.setTooltiptext(createTooltiptext(queueElement));
int startPixels = getStartPixels(datesMapper, queueElement);
result.setLeft(forCSS(startPixels));
if ( startPixels < 0 ) {
cssClass += " truncated-start ";
}
int taskWidth = getWidthPixels(datesMapper, queueElement);
if ( (startPixels + taskWidth) > datesMapper.getHorizontalSize() ) {
taskWidth = datesMapper.getHorizontalSize() - startPixels;
cssClass += " truncated-end ";
} else {
result.appendChild(generateNonWorkableShade(datesMapper, queueElement));
}
result.setWidth(forCSS(taskWidth));
LocalDate deadlineDate = task.getDeadline();
boolean isOrderDeadline = false;
if ( deadlineDate == null ) {
Date orderDate = order.getDeadline();
if ( orderDate != null ) {
deadlineDate = LocalDate.fromDateFields(orderDate);
isOrderDeadline = true;
}
}
if ( deadlineDate != null ) {
int deadlinePosition = getDeadlinePixels(datesMapper, deadlineDate);
if ( deadlinePosition < datesMapper.getHorizontalSize() ) {
Div deadline = new Div();
deadline.setSclass(isOrderDeadline ? "deadline order-deadline" : "deadline");
deadline.setLeft((deadlinePosition - startPixels - DEADLINE_MARK_HALF_WIDTH) + "px");
result.appendChild(deadline);
result.appendChild(generateNonWorkableShade(datesMapper, queueElement));
}
if ( deadlineDate.isBefore(queueElement.getEndDate()) ) {
cssClass += " unmet-deadline ";
}
}
result.setClass(cssClass);
result.appendChild(generateCompletionShade(datesMapper, queueElement));
Component progressBar = generateProgressBar(datesMapper, queueElement);
if ( progressBar != null ) {
result.appendChild(progressBar);
}
return result;
}
private static Component generateProgressBar(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
DateAndHour advancementEndDate = getAdvanceEndDate(queueElement);
if ( advancementEndDate == null ) {
return null;
}
Duration durationBetween = new Duration(
queueElement.getStartTime().toDateTime().getMillis(), advancementEndDate.toDateTime().getMillis());
Div progressBar = new Div();
if ( !queueElement.getStartDate().isEqual(advancementEndDate.getDate()) ) {
progressBar.setWidth(datesMapper.toPixels(durationBetween) + "px");
progressBar.setSclass("queue-progress-bar");
}
return progressBar;
}
private static Div generateNonWorkableShade(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
int workableHours = queueElement
.getLimitingResourceQueue()
.getResource()
.getCalendar()
.getCapacityOn(PartialDay.wholeDay(queueElement.getEndDate()))
.roundToHours();
Long shadeWidth = (24 - workableHours) *
DatesMapperOnInterval.MILISECONDS_PER_HOUR / datesMapper.getMilisecondsPerPixel();
Long lShadeLeft = (workableHours - queueElement.getEndHour()) *
DatesMapperOnInterval.MILISECONDS_PER_HOUR / datesMapper.getMilisecondsPerPixel();
int shadeLeft = lShadeLeft.intValue() + shadeWidth.intValue();
Div notWorkableHoursShade = new Div();
notWorkableHoursShade.setTooltiptext(_("Workable capacity for this period ") + workableHours + _(" hours"));
notWorkableHoursShade.setContext("");
notWorkableHoursShade.setSclass("not-workable-hours");
notWorkableHoursShade.setStyle("left: " + shadeLeft + "px; width: " + shadeWidth.intValue() + "px;");
return notWorkableHoursShade;
}
private static Div generateCompletionShade(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
int workableHours = queueElement
.getLimitingResourceQueue()
.getResource()
.getCalendar()
.getCapacityOn(PartialDay.wholeDay(queueElement.getEndDate()))
.roundToHours();
Long shadeWidth = (24 - workableHours) *
DatesMapperOnInterval.MILISECONDS_PER_HOUR / datesMapper.getMilisecondsPerPixel();
Long lShadeLeft = (workableHours - queueElement.getEndHour()) *
DatesMapperOnInterval.MILISECONDS_PER_HOUR / datesMapper.getMilisecondsPerPixel();
int shadeLeft = lShadeLeft.intValue() + shadeWidth.intValue();
Div notWorkableHoursShade = new Div();
notWorkableHoursShade.setContext("");
notWorkableHoursShade.setSclass("limiting-completion");
notWorkableHoursShade.setStyle("left: " + shadeLeft + "px; width: " + shadeWidth.intValue() + "px;");
return notWorkableHoursShade;
}
private static int getWidthPixels(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
return datesMapper.toPixels(queueElement.getLengthBetween());
}
private static int getDeadlinePixels(IDatesMapper datesMapper, LocalDate deadlineDate) {
// Deadline date is considered inclusively
return datesMapper.toPixelsAbsolute(deadlineDate.plusDays(1).toDateTimeAtStartOfDay().getMillis());
}
private static String forCSS(int pixels) {
return String.format("%dpx", pixels);
}
private static int getStartPixels(IDatesMapper datesMapper, LimitingResourceQueueElement queueElement) {
return datesMapper.toPixelsAbsolute(
queueElement.getStartDate().toDateTimeAtStartOfDay().getMillis() +
queueElement.getStartHour() * DatesMapperOnInterval.MILISECONDS_PER_HOUR);
}
public void appendQueueElements(SortedSet<LimitingResourceQueueElement> elements) {
for (LimitingResourceQueueElement each : elements) {
appendQueueElement(each);
}
}
public void appendQueueElement(LimitingResourceQueueElement element) {
QueueTask queueTask = createQueueTask(element);
appendQueueTask(queueTask);
appendMenu(queueTask);
addDependenciesInPanel(element);
}
public void removeQueueElement(LimitingResourceQueueElement element) {
QueueTask queueTask = findQueueTaskByElement(element);
if ( queueTask != null ) {
removeQueueTask(queueTask);
}
}
private QueueTask findQueueTaskByElement(LimitingResourceQueueElement element) {
for (QueueTask each: queueTasks) {
if ( each.getLimitingResourceQueueElement().getId().equals(element.getId()) ) {
return each;
}
}
return null;
}
private QueueTask createQueueTask(LimitingResourceQueueElement element) {
validateQueueElement(element);
return createDivForElement(timeTracker.getMapper(), element);
}
private void addDependenciesInPanel(LimitingResourceQueueElement element) {
getLimitingResourcesPanel().addDependenciesFor(element);
}
public String getResourceName() {
return limitingResourceQueue.getResource().getName();
}
private static void validateQueueElement(LimitingResourceQueueElement queueElement) {
if ( (queueElement.getStartDate() == null ) || ( queueElement.getEndDate() == null) ) {
throw new ValidationException(_("Invalid queue element"));
}
}
private void appendMenu(QueueTask divElement) {
if ( divElement.getPage() != null ) {
MenuBuilder<QueueTask> menuBuilder = MenuBuilder.on(divElement.getPage(), divElement);
menuBuilder.item(
_("Edit"), "/common/img/ico_editar.png", (chosen, event) -> editResourceAllocation(chosen));
menuBuilder.item(_("Unassign"), "/common/img/ico_borrar.png", (chosen, event) -> unassign(chosen));
menuBuilder.item(_("Move"), "", (chosen, event) -> moveQueueTask(chosen));
divElement.setContext(menuBuilder.createWithoutSettingContext());
}
}
private void editResourceAllocation(QueueTask queueTask) {
getLimitingResourcesPanel().editResourceAllocation(queueTask);
}
private void moveQueueTask(QueueTask queueTask) {
getLimitingResourcesPanel().moveQueueTask(queueTask);
}
private void unassign(QueueTask chosen) {
getLimitingResourcesPanel().unschedule(chosen);
}
private void appendContextMenus() {
for (QueueTask each : queueTasks) {
appendMenu(each);
}
}
public void renderProperties(ContentRenderer renderer) throws IOException{
super.renderProperties(renderer);
render(renderer, "_resourceName", getResourceName());
}
}