/* * 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.montecarlo; import static org.libreplan.web.I18nHelper._; import java.math.BigDecimal; import java.util.HashMap; import java.util.List; import java.util.Map; import org.joda.time.LocalDate; import org.libreplan.business.planner.entities.TaskElement; import org.libreplan.web.common.Util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; import org.zkoss.ganttz.util.LongOperationFeedback; import org.zkoss.ganttz.util.LongOperationFeedback.IBackGroundOperation; import org.zkoss.ganttz.util.LongOperationFeedback.IDesktopUpdate; import org.zkoss.ganttz.util.LongOperationFeedback.IDesktopUpdatesEmitter; import org.zkoss.zk.ui.Executions; import org.zkoss.zk.ui.WrongValueException; import org.zkoss.zk.ui.event.Event; import org.zkoss.zk.ui.event.EventListener; import org.zkoss.zk.ui.event.Events; import org.zkoss.zk.ui.util.GenericForwardComposer; import org.zkoss.zul.Button; import org.zkoss.zul.Checkbox; import org.zkoss.zul.Decimalbox; import org.zkoss.zul.Grid; import org.zkoss.zul.Intbox; import org.zkoss.zul.Label; import org.zkoss.zul.Listbox; import org.zkoss.zul.Listitem; import org.zkoss.zul.Progressmeter; import org.zkoss.zul.Row; import org.zkoss.zul.RowRenderer; import org.zkoss.zul.Rows; import org.zkoss.zul.SimpleListModel; import org.zkoss.zul.Window; import org.zkoss.zul.Constraint; /** * Controller for MonteCarlo graphic generation. * * @author Diego Pino Garcia <dpino@igalia.com> */ @Component @Scope(BeanDefinition.SCOPE_PROTOTYPE) public class MonteCarloController extends GenericForwardComposer { @Autowired private IMonteCarloModel monteCarloModel; private static final Integer DEFAULT_ITERATIONS = 10000; private static final Integer MAX_NUMBER_ITERATIONS = 100000; private final RowRenderer gridCriticalPathTasksRender = new CriticalPathTasksRender(); private Grid gridCriticalPathTasks; private Intbox ibIterations; private Button btnRunMonteCarlo; private Checkbox cbGroupByWeeks; private Listbox lbCriticalPaths; private Progressmeter progressMonteCarloCalculation; private Window monteCarloChartWindow; public MonteCarloController() { } @Override public void doAfterCompose(org.zkoss.zk.ui.Component comp) throws Exception { super.doAfterCompose(comp); ibIterations.setValue(DEFAULT_ITERATIONS); lbCriticalPaths.addEventListener(Events.ON_SELECT, event -> reloadGridCriticalPathTasks()); btnRunMonteCarlo.addEventListener(Events.ON_CLICK, new EventListener() { @Override public void onEvent(Event event) { validateRowsPercentages(); IBackGroundOperation<IDesktopUpdate> operation = this::executeMontecarlo; LongOperationFeedback.progressive(self.getDesktop(), operation); } private void executeMontecarlo(IDesktopUpdatesEmitter<IDesktopUpdate> updatesEmitter) { try { updatesEmitter.doUpdate(disableButton(true)); int iterations = getIterations(); final Map<LocalDate, BigDecimal> monteCarloData = monteCarloModel .calculateMonteCarlo(getSelectedCriticalPath(), iterations, percentageCompletedNotifier(updatesEmitter)); updatesEmitter.doUpdate(showCalculatedData(monteCarloData)); } finally { updatesEmitter.doUpdate(disableButton(false)); } } private IDesktopUpdate disableButton(final boolean disable) { return () -> btnRunMonteCarlo.setDisabled(disable); } private int getIterations() { int iterations = ibIterations.getValue() != null ? ibIterations.getValue().intValue() : 0; if ( iterations == 0 ) { throw new WrongValueException(ibIterations, _("cannot be empty")); } if ( iterations < 0 || iterations > MAX_NUMBER_ITERATIONS ) { throw new WrongValueException(ibIterations, _("Number of iterations should be between 1 and {0}", MAX_NUMBER_ITERATIONS)); } return iterations; } private void validateRowsPercentages() { Intbox intbox; int page = 0; int counter = 0; Rows rows = gridCriticalPathTasks.getRows(); for (Object each : rows.getChildren()) { Row row = (Row) each; List<org.zkoss.zk.ui.Component> children = row.getChildren(); Integer sum = 0; intbox = (Intbox) children.get(3); sum += intbox.getValue(); intbox = (Intbox) children.get(5); sum += intbox.getValue(); intbox = (Intbox) children.get(7); sum += intbox.getValue(); if ( sum != 100 ) { gridCriticalPathTasks.setActivePage(page); throw new WrongValueException(row, _("Percentages should sum 100")); } counter++; if ( counter % gridCriticalPathTasks.getPageSize() == 0 ) { page++; } } } private IDesktopUpdatesEmitter<Integer> percentageCompletedNotifier( final IDesktopUpdatesEmitter<IDesktopUpdate> updatesEmitter) { return new IDesktopUpdatesEmitter<Integer>() { @Override public void doUpdate(final Integer percentage) { updatesEmitter.doUpdate(showCompletedPercentage(percentage)); } private IDesktopUpdate showCompletedPercentage(final Integer value) { return () -> progressMonteCarloCalculation.setValue(value); } }; } private IDesktopUpdate showCalculatedData(final Map<LocalDate, BigDecimal> monteCarloData) { return () -> showMonteCarloGraph(monteCarloData); } private void showMonteCarloGraph(Map<LocalDate, BigDecimal> data) { monteCarloChartWindow = createMonteCarloGraphWindow(data); monteCarloChartWindow.setMode("modal"); } private Window createMonteCarloGraphWindow(Map<LocalDate, BigDecimal> data) { HashMap<String, Object> args = new HashMap<>(); args.put("monteCarloGraphController", new MonteCarloGraphController()); Window result = (Window) Executions.createComponents("/planner/montecarlo_function.zul", self, args); MonteCarloGraphController controller = (MonteCarloGraphController) result.getAttribute("monteCarloGraphController", true); final String orderName = monteCarloModel.getOrderName(); final boolean groupByWeeks = cbGroupByWeeks.isChecked(); controller.generateMonteCarloGraph(orderName, data, groupByWeeks, () -> progressMonteCarloCalculation.setValue(0)); return result; } }); } private void feedCriticalPathsList() { lbCriticalPaths.setModel(new SimpleListModel<>(monteCarloModel.getCriticalPathNames())); if ( !lbCriticalPaths.getChildren().isEmpty() ) { lbCriticalPaths.setSelectedIndex(0); } } private void reloadGridCriticalPathTasks() { List<MonteCarloTask> selectedCriticalPath = getSelectedCriticalPath(); if ( selectedCriticalPath != null ) { gridCriticalPathTasks.setModel(new SimpleListModel<>(selectedCriticalPath)); } if ( gridCriticalPathTasks.getRowRenderer() == null ) { gridCriticalPathTasks.setRowRenderer(gridCriticalPathTasksRender); gridCriticalPathTasks.renderAll(); } } public List<MonteCarloTask> getSelectedCriticalPath() { Listitem selectedItem = lbCriticalPaths.getSelectedItem(); String selectedPath = selectedItem != null ? selectedItem.getLabel() : null; return monteCarloModel.getCriticalPath(selectedPath); } public void setCriticalPath(List<TaskElement> criticalPath) { monteCarloModel.setCriticalPath(criticalPath); if ( lbCriticalPaths != null ) { feedCriticalPathsList(); reloadGridCriticalPathTasks(); } btnRunMonteCarlo.setDisabled(monteCarloModel.getCriticalPathNames().isEmpty()); } public Constraint getCheckConstraintIterationNumber() { return (comp, value) -> { Integer iterationNumber = value != null ? (Integer) value : -1; if (iterationNumber == -1) { throw new WrongValueException(comp, _("cannot be empty")); } else if (iterationNumber < 1 || iterationNumber > 100_000) { throw new WrongValueException(comp, _("Number of iterations should be between 1 and {0}", MAX_NUMBER_ITERATIONS)); } }; } private static class CriticalPathTasksRender implements RowRenderer { @Override public void render(Row row, Object o, int i) throws Exception { row.setValue(o); MonteCarloTask task = (MonteCarloTask) o; row.appendChild(taskName(task)); row.appendChild(duration(task)); row.appendChild(optimisticDuration(task)); row.appendChild(optimisticDurationPercentage(task)); row.appendChild(normalDuration(task)); row.appendChild(normalDurationPercentage(task)); row.appendChild(pessimisticDuration(task)); row.appendChild(pessimisticDurationPercentage(task)); } private Label taskName(final MonteCarloTask task) { return new Label(task.getTaskName()); } private Label duration(final MonteCarloTask task) { Double duration = task.getDuration().doubleValue(); return new Label(duration.toString()); } private Decimalbox pessimisticDuration(final MonteCarloTask task) { Decimalbox result = new Decimalbox(); Util.bind(result, task::getPessimisticDuration, task::setPessimisticDuration); return result; } private Intbox pessimisticDurationPercentage(final MonteCarloTask task) { Intbox result = new Intbox(); Util.bind(result, task::getPessimisticDurationPercentage, task::setPessimisticDurationPercentage); return result; } private Decimalbox normalDuration(final MonteCarloTask task) { Decimalbox result = new Decimalbox(); Util.bind(result, task::getNormalDuration, task::setNormalDuration); return result; } private Intbox normalDurationPercentage(final MonteCarloTask task) { Intbox result = new Intbox(); Util.bind(result, task::getNormalDurationPercentage, task::setNormalDurationPercentage); return result; } private Decimalbox optimisticDuration(final MonteCarloTask task) { Decimalbox result = new Decimalbox(); Util.bind(result, task::getOptimisticDuration, task::setOptimisticDuration); return result; } private Intbox optimisticDurationPercentage(final MonteCarloTask task) { Intbox result = new Intbox(); Util.bind(result, task::getOptimisticDurationPercentage, task::setOptimisticDurationPercentage); return result; } } }