/*
* This file is part of LibrePlan
*
* 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.dashboard;
import static org.libreplan.web.I18nHelper._;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.text.DecimalFormat;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.libreplan.business.orders.entities.Order;
import org.libreplan.business.planner.entities.TaskElement;
import org.libreplan.business.planner.entities.TaskStatusEnum;
import org.libreplan.web.dashboard.DashboardModel.Interval;
import org.libreplan.web.planner.order.PlanningStateCreator.PlanningState;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Scope;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;
import org.zkoss.zk.ui.util.Clients;
import org.zkoss.zk.ui.util.GenericForwardComposer;
import org.zkoss.zkplus.spring.SpringUtil;
import org.zkoss.zul.Div;
import org.zkoss.zul.Label;
import br.com.digilabs.jqplot.Chart;
import br.com.digilabs.jqplot.JqPlotUtils;
import br.com.digilabs.jqplot.chart.BarChart;
import br.com.digilabs.jqplot.chart.PieChart;
import br.com.digilabs.jqplot.elements.Serie;
/**
* Controller for dashboardfororder view.
*
* @author Nacho Barrientos <nacho@igalia.com>
* @author Diego Pino García <dpino@igalia.com>
* @author Vova Perebykivskyi <vova@libreplan-enterprise.com>
*/
@Component
@Scope(BeanDefinition.SCOPE_PROTOTYPE)
public class DashboardController extends GenericForwardComposer {
private IDashboardModel dashboardModel;
private Label lblOvertimeRatio;
private Label lblAvailabilityRatio;
private Label lblAbsolute;
private org.zkoss.zk.ui.Component costStatus;
private Div projectDashboardChartsDiv;
private Div projectDashboardNoTasksWarningDiv;
public DashboardController() {
if ( dashboardModel == null ) {
dashboardModel = (IDashboardModel) SpringUtil.getBean("dashboardModel");
}
}
@Override
public void doAfterCompose(org.zkoss.zk.ui.Component comp) throws Exception {
super.doAfterCompose(comp);
}
public String loadResourceFile(String filename) {
final String newline = "\n";
ApplicationContext ctx = new ClassPathXmlApplicationContext();
Resource res = ctx.getResource(filename);
BufferedReader reader;
StringBuilder sb = new StringBuilder();
try {
reader = new BufferedReader(new InputStreamReader(res.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
sb.append(newline);
}
} catch (IOException e) {
e.printStackTrace();
}
return sb.toString();
}
public void setCurrentOrder(PlanningState planningState, List<TaskElement> criticalPath) {
final Order order = planningState.getOrder();
dashboardModel.setCurrentOrder(planningState, criticalPath);
if (dashboardModel.tasksAvailable()) {
if (self != null) {
renderGlobalProgress();
renderTaskStatus();
renderTaskCompletationLag();
renderDeadlineViolation();
renderMarginWithDeadline();
renderEstimationAccuracy();
renderCostStatus(order);
renderOvertimeRatio();
renderAvailabilityRatio();
}
showCharts();
} else {
hideCharts();
}
}
private void renderOvertimeRatio() {
BigDecimal overtimeRatio = dashboardModel.getOvertimeRatio();
lblOvertimeRatio.setValue(showAsPercentage(overtimeRatio));
String valueMeaning = (overtimeRatio.compareTo(BigDecimal.ZERO) == 0) ? "positive" : "negative";
lblOvertimeRatio.setSclass("dashboard-label-remarked " + valueMeaning);
}
private String showAsPercentage(BigDecimal overtimeRatio) {
return overtimeRatio.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP) + " %";
}
private void renderAvailabilityRatio() {
lblAvailabilityRatio.setValue(showAsPercentage(dashboardModel.getAvailabilityRatio()));
}
private void renderCostStatus(Order order) {
CostStatusController costStatusController = getCostStatusController();
costStatusController.setOrder(order);
costStatusController.render();
}
private CostStatusController getCostStatusController() {
return (CostStatusController) costStatus.getAttribute("controller");
}
private void renderMarginWithDeadline() {
Integer absoluteMargin = dashboardModel.getAbsoluteMarginWithDeadLine();
BigDecimal relativeMargin = dashboardModel.getMarginWithDeadLine();
if ( (lblAbsolute != null) && (absoluteMargin != null) ) {
lblAbsolute.setValue(_(
"There is a margin of {0} days with the project global deadline ({1}%).",
absoluteMargin,
(new DecimalFormat("#.##")).format(relativeMargin.doubleValue() * 100)));
} else {
lblAbsolute.setValue(_("No project deadline defined"));
}
}
private void renderDeadlineViolation() {
final String divId = "deadline-violation";
PieChart<Number> pieChart = new PieChart<>(_("Task deadline violations"));
pieChart.addValue(_("On schedule"), dashboardModel.getPercentageOfOnScheduleTasks());
pieChart.addValue(_("Violated deadline"), dashboardModel.getPercentageOfTasksWithViolatedDeadline());
pieChart.addValue(_("No deadline"), dashboardModel.getPercentageOfTasksWithNoDeadline());
pieChart.addIntervalColors("#8fbe86", "#eb6b71", "#cfcfcf");
renderPieChart(pieChart, divId);
}
/**
* Use this method to render a {@link PieChart}.
*
* FIXME:
* jqplot4java doesn't include a method for changing the colors or a {@link PieChart}.
* The only way to do it is to add the colors to an
* Interval, generate the output Javascript code and replace the string 'intervalColors' by 'seriesColors'.
*
* @param chart
* @param divId
*/
private void renderPieChart(Chart<?> chart, String divId) {
String jsCode = JqPlotUtils.createJquery(chart, divId);
jsCode = jsCode.replace("intervalColors", "seriesColors");
Clients.evalJavaScript(jsCode);
}
private void renderChart(Chart<?> chart, String divId) {
String jsCode = JqPlotUtils.createJquery(chart, divId);
Clients.evalJavaScript(jsCode);
}
private void renderTaskCompletationLag() {
final String divId = "task-completation-lag";
BarChart<Integer> barChart;
barChart = new BarChart<>(_("Task Completation Lead/Lag"));
barChart.setFillZero(true);
barChart.setHighlightMouseDown(true);
barChart.setStackSeries(false);
barChart.setBarMargin(30);
barChart.addSeries(new Serie("Tasks"));
TaskCompletionData taskCompletionData = TaskCompletionData.create(dashboardModel);
// TODO resolve deprecated
barChart.setTicks(taskCompletionData.getTicks());
barChart.addValues(taskCompletionData.getValues());
barChart.getAxes()
.getXaxis()
.setLabel(_("Days Interval (Calculated as task completion end date minus estimated end date)"));
barChart.getAxes().yAxisInstance().setLabel(_("Number of tasks"));
renderChart(barChart, divId);
}
private void renderEstimationAccuracy() {
final String divId = "estimation-accuracy";
BarChart<Integer> barChart;
barChart = new BarChart<>(_("Estimation deviation on completed tasks"));
barChart.setFillZero(true);
barChart.setHighlightMouseDown(true);
barChart.setStackSeries(false);
barChart.setBarMargin(30);
barChart.addSeries(new Serie("Tasks"));
EstimationAccuracy estimationAccuracyData = EstimationAccuracy.create(dashboardModel);
// TODO resolve deprecated
barChart.setTicks(estimationAccuracyData.getTicks());
barChart.addValues(estimationAccuracyData.getValues());
barChart.getAxes()
.getXaxis()
.setLabel(_("% Deviation interval (difference % between consumed and estimated hours)"));
barChart.getAxes().yAxisInstance().setLabel(_("Number of tasks"));
renderChart(barChart, divId);
}
private String statusLegend(TaskStatusEnum status, Map<TaskStatusEnum, Integer> taskStatus) {
return _(status.toString()) + String.format(_(" (%d tasks)"), taskStatus.get(status));
}
private void renderTaskStatus() {
final String divId = "task-status";
Map<TaskStatusEnum, Integer> taskStatus = dashboardModel.calculateTaskStatus();
PieChart<Number> taskStatusPieChart = new PieChart<>(_("Task Status"));
taskStatusPieChart.addValue(
statusLegend(TaskStatusEnum.FINISHED, taskStatus),
dashboardModel.getPercentageOfFinishedTasks());
taskStatusPieChart.addValue(
statusLegend(TaskStatusEnum.IN_PROGRESS, taskStatus),
dashboardModel.getPercentageOfInProgressTasks());
taskStatusPieChart.addValue(
statusLegend(TaskStatusEnum.READY_TO_START, taskStatus),
dashboardModel.getPercentageOfReadyToStartTasks());
taskStatusPieChart.addValue(
statusLegend(TaskStatusEnum.BLOCKED, taskStatus),
dashboardModel.getPercentageOfBlockedTasks());
taskStatusPieChart.addIntervalColors("#d599e8", "#4c99e8", "#8fbe86", "#ffbb6b");
renderPieChart(taskStatusPieChart, divId);
}
private void renderGlobalProgress() {
GlobalProgressChart globalProgressChart = GlobalProgressChart.create();
// Current values
globalProgressChart.current(
GlobalProgressChart.CRITICAL_PATH_DURATION,
dashboardModel.getCriticalPathProgressByDuration());
globalProgressChart.current(
GlobalProgressChart.CRITICAL_PATH_HOURS,
dashboardModel.getCriticalPathProgressByNumHours());
globalProgressChart.current(
GlobalProgressChart.ALL_TASKS_HOURS,
dashboardModel.getAdvancePercentageByHours());
globalProgressChart.current(
GlobalProgressChart.SPREAD_PROGRESS,
dashboardModel.getSpreadProgress());
// Expected values
globalProgressChart.expected(
GlobalProgressChart.CRITICAL_PATH_DURATION,
dashboardModel.getExpectedCriticalPathProgressByDuration());
globalProgressChart.expected(
GlobalProgressChart.CRITICAL_PATH_HOURS,
dashboardModel.getExpectedCriticalPathProgressByNumHours());
globalProgressChart.expected(
GlobalProgressChart.ALL_TASKS_HOURS,
dashboardModel.getExpectedAdvancePercentageByHours());
globalProgressChart.expected(
GlobalProgressChart.SPREAD_PROGRESS,
BigDecimal.ZERO);
globalProgressChart.render();
}
private void showCharts() {
projectDashboardChartsDiv.setVisible(true);
projectDashboardNoTasksWarningDiv.setVisible(false);
}
private void hideCharts() {
projectDashboardChartsDiv.setVisible(false);
projectDashboardNoTasksWarningDiv.setVisible(true);
}
/**
* @author Diego Pino García<dpino@igalia.com>
*/
static class TaskCompletionData {
private final IDashboardModel dashboardModel;
private Map<Interval, Integer> taskCompletionData;
private TaskCompletionData(IDashboardModel dashboardModel) {
this.dashboardModel = dashboardModel;
}
public static TaskCompletionData create(IDashboardModel dashboardModel) {
return new TaskCompletionData(dashboardModel);
}
private Map<Interval, Integer> getData() {
if ( taskCompletionData == null) {
taskCompletionData = dashboardModel.calculateTaskCompletion();
}
return taskCompletionData;
}
String[] getTicks() {
Set<Interval> intervals = getData().keySet();
String[] result = new String[intervals.size()];
int i = 0;
for (Interval each : intervals) {
result[i++] = each.toString();
}
return result;
}
public Collection<Integer> getValues() {
return getData().values();
}
}
/**
* @author Diego Pino García<dpino@igalia.com>
*/
static class EstimationAccuracy {
private final IDashboardModel dashboardModel;
private Map<Interval, Integer> estimationAccuracyData;
private EstimationAccuracy(IDashboardModel dashboardModel) {
this.dashboardModel = dashboardModel;
}
public static EstimationAccuracy create(IDashboardModel dashboardModel) {
return new EstimationAccuracy(dashboardModel);
}
private Map<Interval, Integer> getData() {
if (estimationAccuracyData == null) {
estimationAccuracyData = dashboardModel.calculateEstimationAccuracy();
}
return estimationAccuracyData;
}
String[] getTicks() {
Set<Interval> intervals = getData().keySet();
String[] result = new String[intervals.size()];
int i = 0;
for (Interval each : intervals) {
result[i++] = each.toString();
}
return result;
}
public Collection<Integer> getValues() {
return getData().values();
}
}
}