/*
* 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 java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.joda.time.Period;
import org.libreplan.business.planner.limiting.entities.LimitingResourceQueueElement;
import org.libreplan.business.resources.entities.LimitingResourceQueue;
import org.libreplan.web.common.Util;
import org.zkoss.ganttz.DependencyList;
import org.zkoss.ganttz.timetracker.TimeTracker;
import org.zkoss.ganttz.timetracker.TimeTracker.IDetailItemFilter;
import org.zkoss.ganttz.timetracker.TimeTrackerComponent;
import org.zkoss.ganttz.timetracker.zoom.DetailItem;
import org.zkoss.ganttz.timetracker.zoom.IZoomLevelChangedListener;
import org.zkoss.ganttz.timetracker.zoom.ZoomLevel;
import org.zkoss.ganttz.util.ComponentsFinder;
import org.zkoss.ganttz.util.Interval;
import org.zkoss.ganttz.util.MutableTreeModel;
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.HtmlMacroComponent;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zul.Button;
import org.zkoss.zul.ListModel;
import org.zkoss.zul.Listbox;
import org.zkoss.zul.Listitem;
import org.zkoss.zul.Separator;
import org.zkoss.zul.SimpleListModel;
public class LimitingResourcesPanel extends HtmlMacroComponent {
public interface IToolbarCommand {
void doAction();
String getLabel();
String getImage();
}
private LimitingResourcesController limitingResourcesController;
private TimeTracker timeTracker;
private TimeTrackerComponent timeTrackerComponent;
private TimeTrackerComponent timeTrackerHeader;
private LimitingResourcesLeftPane leftPane;
private QueueListComponent queueListComponent;
private Listbox listZoomLevels;
private Button paginationDownButton;
private Button paginationUpButton;
private Listbox horizontalPagination;
private LimitingDependencyList dependencyList = new LimitingDependencyList(this);
private PaginatorFilter paginatorFilter;
private Component insertionPointLeftPanel;
private Component insertionPointRightPanel;
private Component insertionPointTimetracker;
private LocalDate previousStart;
private Interval previousInterval;
public LimitingResourcesPanel(LimitingResourcesController limitingResourcesController, TimeTracker timeTracker) {
init(limitingResourcesController, timeTracker);
}
public void paginationDown() {
horizontalPagination.setSelectedIndex(horizontalPagination.getSelectedIndex() - 1);
goToSelectedHorizontalPage();
}
public void paginationUp() {
horizontalPagination.setSelectedIndex(Math.max(1, horizontalPagination.getSelectedIndex() + 1));
goToSelectedHorizontalPage();
}
/**
* Returns the closest upper {@link LimitingResourcesPanel} instance going all the way up from comp.
*
* @param comp
* @return {@link LimitingResourcesPanel}
*/
public static LimitingResourcesPanel getLimitingResourcesPanel(Component comp) {
if (comp == null) {
return null;
}
if (comp instanceof LimitingResourcesPanel) {
return (LimitingResourcesPanel) comp;
}
return getLimitingResourcesPanel(comp.getParent());
}
public void init(LimitingResourcesController limitingResourcesController, TimeTracker timeTracker) {
this.limitingResourcesController = limitingResourcesController;
this.timeTracker = timeTracker;
this.setAttribute("limitingResourcesController", limitingResourcesController, true);
MutableTreeModel<LimitingResourceQueue> treeModel = createModelForTree();
queueListComponent = new QueueListComponent(this, timeTracker, treeModel);
leftPane = new LimitingResourcesLeftPane(treeModel);
setAuService(new AuService() {
public boolean service(AuRequest request, boolean everError) {
String command = request.getCommand();
int zoomindex;
int scrollLeft;
if ( "onZoomLevelChange".equals(command) ) {
zoomindex= (Integer) retrieveData(request, "zoomindex");
scrollLeft = (Integer) retrieveData(request, "scrollLeft");
setZoomLevel(
(ZoomLevel)((Listbox) getFellow("listZoomLevels")).getModel().getElementAt(zoomindex),
scrollLeft );
return true;
}
return false;
}
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;
}
});
}
public void setZoomLevel(ZoomLevel zoomLevel, int scrollLeft) {
savePreviousData();
getTimeTrackerComponent().updateDayScroll();
getTimeTrackerComponent().setZoomLevel(zoomLevel, scrollLeft);
}
public void appendQueueElementToQueue(LimitingResourceQueueElement element) {
queueListComponent.appendQueueElement(element);
dependencyList.addDependenciesFor(element);
}
public void removeQueueElementFrom(LimitingResourceQueue queue, LimitingResourceQueueElement element) {
queueListComponent.removeQueueElementFrom(queue, element);
dependencyList.removeDependenciesFor(element);
}
private MutableTreeModel<LimitingResourceQueue> createModelForTree() {
MutableTreeModel<LimitingResourceQueue> result = MutableTreeModel.create(LimitingResourceQueue.class);
for (LimitingResourceQueue LimitingResourceQueue : getLimitingResourceQueues()) {
result.addToRoot(LimitingResourceQueue);
}
return result;
}
private List<LimitingResourceQueue> getLimitingResourceQueues() {
return limitingResourcesController.getLimitingResourceQueues();
}
public ListModel getZoomLevels() {
ZoomLevel[] selectableZoomlevels = {
ZoomLevel.DETAIL_THREE,
ZoomLevel.DETAIL_FOUR,
ZoomLevel.DETAIL_FIVE,
};
return new SimpleListModel<>(selectableZoomlevels);
}
public void setZoomLevel(final ZoomLevel zoomLevel) {
savePreviousData();
getTimeTrackerComponent().updateDayScroll();
timeTracker.setZoomLevel(zoomLevel);
}
public void zoomIncrease() {
savePreviousData();
getTimeTrackerComponent().updateDayScroll();
timeTracker.zoomIncrease();
}
public void zoomDecrease() {
savePreviousData();
getTimeTrackerComponent().updateDayScroll();
timeTracker.zoomDecrease();
}
public void add(final IToolbarCommand... commands) {
Component toolbar = getToolbar();
Separator separator = getSeparator();
for (IToolbarCommand c : commands) {
toolbar.insertBefore(asButton(c), separator);
}
}
private Button asButton(final IToolbarCommand c) {
Button result = new Button();
result.addEventListener(Events.ON_CLICK, event -> c.doAction());
if (!StringUtils.isEmpty(c.getImage())) {
result.setImage(c.getImage());
result.setTooltiptext(c.getLabel());
} else {
result.setLabel(c.getLabel());
}
return result;
}
@SuppressWarnings("unchecked")
private Separator getSeparator() {
return ComponentsFinder.findComponentsOfType(Separator.class, getToolbar().getChildren()).get(0);
}
private Component getToolbar() {
return getFellow("toolbar");
}
@Override
public void afterCompose() {
super.afterCompose();
initializeBindings();
initializeTimetracker();
listZoomLevels.setSelectedIndex(timeTracker.getDetailLevel().ordinal() - 2);
// Insert leftPane component with limitingresources list
insertionPointLeftPanel.appendChild(leftPane);
leftPane.afterCompose();
// Initialize queues
insertionPointRightPanel.appendChild(queueListComponent);
queueListComponent.afterCompose();
// Initialize dependencies
rebuildDependencies();
dependencyList.afterCompose();
initializePagination();
}
/**
* Apparently it's necessary to append {@link DependencyList} to
* insertionPointRightPanel again every time the list of dependencies is regenerated.
* Otherwise tasks overflow if they don't fit in current page.
*/
private void rebuildDependencies() {
dependencyList.clear();
for (LimitingResourceQueueElement each : getLimitingResourceQueueElements()) {
dependencyList.addDependenciesFor(each);
}
insertionPointRightPanel.appendChild(dependencyList);
}
private Set<LimitingResourceQueueElement> getLimitingResourceQueueElements() {
return queueListComponent.getLimitingResourceElementToQueueTaskMap().keySet();
}
private void initializeTimetracker() {
timeTracker.addZoomListener(new IZoomLevelChangedListener() {
@Override
public void zoomLevelChanged(ZoomLevel newDetailLevel) {
reloadTimetracker();
reloadComponent();
}
private void reloadTimetracker() {
timeTracker.resetMapper();
paginatorFilter.setInterval(timeTracker.getRealInterval());
timeTracker.setFilter(paginatorFilter);
paginatorFilter.populateHorizontalListbox();
paginatorFilter.goToHorizontalPage(0);
adjustZoomPositionScroll();
}
});
timeTrackerHeader = createTimeTrackerHeader();
timeTrackerComponent = createTimeTrackerComponent();
insertionPointTimetracker.appendChild(timeTrackerHeader);
insertionPointRightPanel.appendChild(timeTrackerComponent);
timeTrackerHeader.afterCompose();
timeTrackerComponent.afterCompose();
}
@SuppressWarnings("serial")
private TimeTrackerComponent createTimeTrackerHeader() {
return new TimeTrackerComponent(timeTracker) {
@Override
protected void scrollHorizontalPercentage(int pixelsDisplacement) {}
@Override
protected void moveCurrentPositionScroll() {}
@Override
protected void updateCurrentDayScroll() {}
};
}
private void adjustZoomPositionScroll() {
getTimeTrackerComponent().movePositionScroll();
}
@SuppressWarnings("serial")
private TimeTrackerComponent createTimeTrackerComponent() {
return new TimeTrackerComponent(timeTracker) {
@Override
protected void scrollHorizontalPercentage(int pixelsDisplacement) {
response("", new AuInvoke(
queueListComponent, "adjustScrollHorizontalPosition", Integer.toString(pixelsDisplacement)));
}
@Override
protected void moveCurrentPositionScroll() {}
@Override
protected void updateCurrentDayScroll() {}
};
}
private void initializePagination() {
paginatorFilter = new PaginatorFilter();
paginationUpButton.setDisabled(paginatorFilter.isLastPage());
paginatorFilter.setInterval(timeTracker.getRealInterval());
timeTracker.setFilter(paginatorFilter);
paginatorFilter.populateHorizontalListbox();
}
private void initializeBindings() {
// Zoom and pagination
listZoomLevels = (Listbox) getFellow("listZoomLevels");
horizontalPagination = (Listbox) getFellow("horizontalPagination");
paginationUpButton = (Button) getFellow("paginationUpButton");
paginationDownButton = (Button) getFellow("paginationDownButton");
insertionPointLeftPanel = getFellow("insertionPointLeftPanel");
insertionPointRightPanel = getFellow("insertionPointRightPanel");
insertionPointTimetracker = getFellow("insertionPointTimetracker");
}
public Map<LimitingResourceQueueElement, QueueTask> getQueueTaskMap() {
return queueListComponent.getLimitingResourceElementToQueueTaskMap();
}
public void clearComponents() {
getFellow("insertionPointLeftPanel").getChildren().clear();
getFellow("insertionPointRightPanel").getChildren().clear();
getFellow("insertionPointTimetracker").getChildren().clear();
}
public TimeTrackerComponent getTimeTrackerComponent() {
return timeTrackerComponent;
}
public TimeTracker getTimeTracker() {
return timeTrackerComponent.getTimeTracker();
}
public void unschedule(QueueTask task) {
LimitingResourceQueueElement queueElement = task.getLimitingResourceQueueElement();
LimitingResourceQueue queue = queueElement.getLimitingResourceQueue();
limitingResourcesController.unschedule(task);
removeQueueTask(task);
dependencyList.removeDependenciesFor(queueElement);
queueListComponent.removeQueueElementFrom(queue, queueElement);
}
private void removeQueueTask(QueueTask task) {
task.detach();
}
public void moveQueueTask(QueueTask queueTask) {
if (limitingResourcesController.moveTask(queueTask.getLimitingResourceQueueElement())) {
removeQueueTask(queueTask);
}
}
public void editResourceAllocation(QueueTask queueTask) {
limitingResourcesController.editResourceAllocation(queueTask.getLimitingResourceQueueElement());
}
public void removeDependenciesFor(LimitingResourceQueueElement element) {
dependencyList.removeDependenciesFor(element);
}
public void addDependenciesFor(LimitingResourceQueueElement element) {
dependencyList.addDependenciesFor(element);
}
public void refreshQueues(Collection<LimitingResourceQueue> queues) {
for (LimitingResourceQueue each: queues) {
refreshQueue(each);
}
}
public void refreshQueue(LimitingResourceQueue queue) {
dependencyList.removeDependenciesFor(queue);
queueListComponent.refreshQueue(queue);
}
public void goToSelectedHorizontalPage() {
paginatorFilter.goToHorizontalPage(horizontalPagination.getSelectedIndex());
reloadComponent();
}
public void reloadComponent() {
refreshTimetracker();
refreshQueueComponents();
rebuildDependencies();
}
private void refreshTimetracker() {
timeTrackerHeader.recreate();
timeTrackerComponent.recreate();
}
private void refreshQueueComponents() {
queueListComponent.invalidate();
queueListComponent.afterCompose();
queueListComponent.refreshQueues();
rebuildDependencies();
}
private class PaginatorFilter implements IDetailItemFilter {
private DateTime intervalStart;
private DateTime intervalEnd;
private DateTime paginatorStart;
private DateTime paginatorEnd;
@Override
public Interval getCurrentPaginationInterval() {
return new Interval(paginatorStart.toDate(), paginatorEnd.toDate());
}
private Period intervalIncrease() {
switch (timeTracker.getDetailLevel()) {
case DETAIL_ONE:
case DETAIL_TWO:
return Period.years(5);
case DETAIL_THREE:
return Period.years(2);
case DETAIL_FOUR:
return Period.months(12);
case DETAIL_FIVE:
return Period.weeks(12);
default:
// Default month
return Period.years(2);
}
}
public void setInterval(Interval realInterval) {
intervalStart = realInterval.getStart().toDateTimeAtStartOfDay();
intervalEnd = realInterval.getFinish().toDateTimeAtStartOfDay();
paginatorStart = intervalStart;
paginatorEnd = intervalStart.plus(intervalIncrease());
if (paginatorEnd.plus(intervalIncrease()).isAfter(intervalEnd)) {
paginatorEnd = intervalEnd;
}
updatePaginationButtons();
dependencyList.recreateDependencyComponents();
}
@Override
public void resetInterval() {
setInterval(timeTracker.getRealInterval());
}
@Override
public Collection<DetailItem> selectsFirstLevel(Collection<DetailItem> firstLevelDetails) {
ArrayList<DetailItem> result = new ArrayList<>();
boolean itemIsNotBeforePaginatorStartAndAfterEnd;
for (DetailItem each : firstLevelDetails) {
itemIsNotBeforePaginatorStartAndAfterEnd = (each.getStartDate() == null) ||
!(each.getStartDate().isBefore(paginatorStart)) &&
(each.getStartDate().isBefore(paginatorEnd));
if ( itemIsNotBeforePaginatorStartAndAfterEnd ) {
result.add(each);
}
}
return result;
}
@Override
public Collection<DetailItem> selectsSecondLevel(Collection<DetailItem> secondLevelDetails) {
ArrayList<DetailItem> result = new ArrayList<>();
boolean itemIsNotBeforePaginatorStartAndAfterEnd;
for (DetailItem each : secondLevelDetails) {
itemIsNotBeforePaginatorStartAndAfterEnd = (each.getStartDate() == null) ||
!(each.getStartDate().isBefore(paginatorStart)) &&
(each.getStartDate().isBefore(paginatorEnd));
if ( itemIsNotBeforePaginatorStartAndAfterEnd ) {
result.add(each);
}
}
return result;
}
public void populateHorizontalListbox() {
horizontalPagination.getItems().clear();
DateTime intervalStart = timeTracker.getRealInterval().getStart().toDateTimeAtStartOfDay();
if (intervalStart != null) {
DateTime itemStart = intervalStart;
DateTime itemEnd = intervalStart.plus(intervalIncrease());
while (intervalEnd.isAfter(itemStart)) {
if (intervalEnd.isBefore(itemEnd) || !intervalEnd.isAfter(itemEnd.plus(intervalIncrease()))) {
itemEnd = intervalEnd;
}
Listitem item =
new Listitem(Util.formatDate(itemStart) + " - " + Util.formatDate(itemEnd.minusDays(1)));
horizontalPagination.appendChild(item);
itemStart = itemEnd;
itemEnd = itemEnd.plus(intervalIncrease());
}
}
horizontalPagination.setSelectedIndex(0);
// Disable pagination if there's only one page
int size = horizontalPagination.getItems().size();
horizontalPagination.setDisabled(size == 1);
}
public void goToHorizontalPage(int interval) {
paginatorStart = intervalStart;
paginatorStart = timeTracker.getDetailsFirstLevel().iterator().next().getStartDate();
for (int i = 0; i < interval; i++) {
paginatorStart = paginatorStart.plus(intervalIncrease());
}
paginatorEnd = paginatorStart.plus(intervalIncrease());
if (paginatorEnd.plus(intervalIncrease()).isAfter(intervalEnd)) {
paginatorEnd = paginatorEnd.plus(intervalIncrease());
}
timeTracker.resetMapper();
updatePaginationButtons();
}
private void updatePaginationButtons() {
paginationDownButton.setDisabled(isFirstPage());
paginationUpButton.setDisabled(isLastPage());
}
public boolean isFirstPage() {
return (horizontalPagination.getSelectedIndex() <= 0) || horizontalPagination.isDisabled();
}
private boolean isLastPage() {
return (horizontalPagination.getItemCount() == (horizontalPagination.getSelectedIndex() + 1)) ||
horizontalPagination.isDisabled();
}
}
private void savePreviousData() {
TimeTracker tracker = getTimeTrackerComponent().getTimeTracker();
this.previousStart = tracker.getRealInterval().getStart();
this.previousInterval = tracker.getMapper().getInterval();
}
public LocalDate getPreviousStart() {
return previousStart;
}
public Interval getPreviousInterval() {
return previousInterval;
}
public String getWidgetClass() {
return "limitingresources.LimitingResourcesPanel";
}
}