/*******************************************************************************
* Copyright (c) 2014 Open Door Logistics (www.opendoorlogistics.com)
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser Public License v3
* which accompanies this distribution, and is available at http://www.gnu.org/licenses/lgpl.txt
******************************************************************************/
package com.opendoorlogistics.components.gantt;
import java.awt.Color;
import java.awt.Paint;
import java.awt.Shape;
import java.awt.geom.Ellipse2D;
import java.io.Serializable;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import javax.swing.Icon;
import javax.swing.JPanel;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.LegendItem;
import org.jfree.chart.LegendItemCollection;
import org.jfree.chart.StandardChartTheme;
import org.jfree.chart.axis.CategoryAxis;
import org.jfree.chart.axis.DateAxis;
import org.jfree.chart.entity.CategoryItemEntity;
import org.jfree.chart.entity.EntityCollection;
import org.jfree.chart.labels.CategoryToolTipGenerator;
import org.jfree.chart.plot.CategoryPlot;
import org.jfree.chart.plot.PlotOrientation;
import org.jfree.chart.renderer.category.CategoryItemRenderer;
import org.jfree.chart.renderer.category.GanttRenderer;
import org.jfree.chart.renderer.category.StandardBarPainter;
import org.jfree.chart.urls.CategoryURLGenerator;
import org.jfree.chart.util.ParamChecks;
import org.jfree.data.category.CategoryDataset;
import org.jfree.data.gantt.Task;
import org.jfree.data.gantt.TaskSeries;
import org.jfree.data.gantt.TaskSeriesCollection;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.StringConventions;
import com.opendoorlogistics.api.components.ComponentConfigurationEditorAPI;
import com.opendoorlogistics.api.components.ComponentControlLauncherApi;
import com.opendoorlogistics.api.components.ComponentControlLauncherApi.ControlLauncherCallback;
import com.opendoorlogistics.api.components.ComponentExecutionApi;
import com.opendoorlogistics.api.components.ODLComponent;
import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder;
import com.opendoorlogistics.api.standardcomponents.GanntChart;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLDatastoreAlterable;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.ODLTableAlterable;
import com.opendoorlogistics.api.tables.ODLTableDefinition;
import com.opendoorlogistics.api.tables.ODLTime;
import com.opendoorlogistics.api.ui.Disposable;
import com.opendoorlogistics.core.utils.Colours;
import com.opendoorlogistics.core.utils.Colours.CalculateAverageColour;
import com.opendoorlogistics.utils.ui.Icons;
public class GanttChartComponent implements ODLComponent, GanntChart {
private static class MySubtask extends Task{
private final GanttItem item;
MySubtask(GanttItem item, Date start, Date end) {
super("", start, end);
this.item = item;
}
GanttItem getItem(){
return item;
}
}
@Override
public String getId() {
return "com.opendoorlogistics.components.gantt";
}
@Override
public String getName() {
return "Gantt chart";
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition(ODLApi api, Serializable configuration) {
return GanttItem.beanMapping.getDefinition();
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getOutputDsDefinition(ODLApi api, int mode, Serializable configuration) {
// TODO Auto-generated method stub
return null;
}
@Override
public void execute(final ComponentExecutionApi api, int mode, Object configuration, ODLDatastore<? extends ODLTable> ioDs, ODLDatastoreAlterable<? extends ODLTableAlterable> outputDs) {
// Get items and sort by resource then date
final StringConventions sc = api.getApi().stringConventions();
List<GanttItem> items = GanttItem.beanMapping.getTableMapping(0).readObjectsFromTable(ioDs.getTableAt(0));
// Rounding doubles to longs can create small errors where a start time is 1 millisecond after an end.
// Set all start times to be <= end time
for(GanttItem item:items){
if(item.getStart()==null || item.getEnd()==null){
throw new RuntimeException("Found Gannt item with null start or end time.");
}
if(item.getStart().getValue() > item.getEnd().getValue()){
item.setStart(item.getEnd());
}
}
Collections.sort(items, new Comparator<GanttItem>() {
@Override
public int compare(GanttItem o1, GanttItem o2) {
int diff = sc.compareStandardised(o1.getResourceId(), o2.getResourceId());
if (diff == 0) {
diff = o1.getStart().compareTo(o2.getStart());
}
if (diff == 0) {
diff = o1.getEnd().compareTo(o2.getEnd());
}
if (diff == 0) {
diff = sc.compareStandardised(o1.getActivityId(), o2.getActivityId());
}
if (diff == 0) {
diff = Colours.compare(o1.getColor(), o2.getColor());
}
return diff;
}
});
// Filter any zero duration items
Iterator<GanttItem> it = items.iterator();
while (it.hasNext()) {
GanttItem item = it.next();
if (item.getStart().compareTo(item.getEnd()) == 0) {
it.remove();
}
}
// Get average colour for each activity type
Map<String, CalculateAverageColour> calcColourMap = api.getApi().stringConventions().createStandardisedMap();
for (GanttItem item : items) {
CalculateAverageColour calc = calcColourMap.get(item.getActivityId());
if (calc == null) {
calc = new CalculateAverageColour();
calcColourMap.put(item.getActivityId(), calc);
}
calc.add(item.getColor());
}
// Put into colour map
Map<String, Color> colourMap = api.getApi().stringConventions().createStandardisedMap();
for (Map.Entry<String, CalculateAverageColour> entry : calcColourMap.entrySet()) {
colourMap.put(entry.getKey(), entry.getValue().getAverage());
}
// Split items by resource
ArrayList<ArrayList<GanttItem>> splitByResource = new ArrayList<>();
ArrayList<GanttItem> current = null;
for (GanttItem item : items) {
if (current == null || sc.compareStandardised(current.get(0).getResourceId(), item.getResourceId()) != 0) {
current = new ArrayList<>();
splitByResource.add(current);
}
current.add(item);
}
// put into jfreechart's task data structure
TaskSeries ts = new TaskSeries("Resources");
for (ArrayList<GanttItem> resource : splitByResource) {
// get earliest and latest time (last time may not be in the last item)
ODLTime earliest = resource.get(0).getStart();
ODLTime latest = null;
for (GanttItem item : resource) {
if (latest == null || latest.compareTo(item.getEnd()) < 0) {
latest = item.getEnd();
}
}
Task task = new Task(resource.get(0).getResourceId(), new Date(earliest.getTotalMilliseconds()), new Date(latest.getTotalMilliseconds()));
// add all items as subtasks
for (GanttItem item : resource) {
task.addSubtask(new MySubtask(item, new Date(item.getStart().getTotalMilliseconds()), new Date(item.getEnd().getTotalMilliseconds())));
}
ts.add(task);
}
TaskSeriesCollection collection = new TaskSeriesCollection();
collection.add(ts);
// Create the plot
CategoryAxis categoryAxis = new CategoryAxis(null);
DateAxis dateAxis = new DateAxis("Time");
CategoryItemRenderer renderer = new MyRenderer(collection, colourMap);
final CategoryPlot plot = new CategoryPlot(collection, categoryAxis, dateAxis, renderer);
plot.setOrientation(PlotOrientation.HORIZONTAL);
plot.getDomainAxis().setLabel(null);
((DateAxis) plot.getRangeAxis()).setDateFormatOverride(new DateFormat() {
@Override
public Date parse(String source, ParsePosition pos) {
// TODO Auto-generated method stub
return null;
}
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
toAppendTo.append(new ODLTime(date.getTime()).toString());
return toAppendTo;
}
});
// Create the chart and apply the standard theme without shadows to it
api.submitControlLauncher(new ControlLauncherCallback() {
@Override
public void launchControls(ComponentControlLauncherApi launcherApi) {
JFreeChart chart = new JFreeChart(null, JFreeChart.DEFAULT_TITLE_FONT, plot, true);
StandardChartTheme theme = new StandardChartTheme("standard theme", false);
theme.setBarPainter(new StandardBarPainter());
theme.apply(chart);
class MyPanel extends ChartPanel implements Disposable {
public MyPanel(JFreeChart chart) {
super(chart);
}
@Override
public void dispose() {
// TODO Auto-generated method stub
}
}
MyPanel chartPanel = new MyPanel(chart);
launcherApi.registerPanel("Resource Gantt", null, chartPanel, true);
}
});
}
/** @see http://stackoverflow.com/questions/8938690 */
private static class MyRenderer extends GanttRenderer {
private static final int PASS = 1; // currently have one pass
private final List<Color> colours = new ArrayList<Color>();
private final List<String> tooltips = new ArrayList<String>();
private final TaskSeriesCollection model;
private final Map<String, Color> colourMap;
private int row;
private int col;
private int index;
public MyRenderer(TaskSeriesCollection model, Map<String, Color> colourMap) {
this.model = model;
this.colourMap = colourMap;
}
/**
* Override the method so we can set custom tooltips
*/
@Override
protected void addItemEntity(EntityCollection entities,
CategoryDataset dataset, int row, int column, Shape hotspot) {
ParamChecks.nullNotPermitted(hotspot, "hotspot");
if (!getItemCreateEntity(row, column)) {
return;
}
String tip = null;
if((index-1) < tooltips.size()){
tip = tooltips.get(index-1);
}
String url = null;
CategoryURLGenerator urlster = getItemURLGenerator(row, column);
if (urlster != null) {
url = urlster.generateURL(dataset, row, column);
}
CategoryItemEntity entity = new CategoryItemEntity(hotspot, tip, url,
dataset, dataset.getRowKey(row), dataset.getColumnKey(column));
entities.add(entity);
}
@Override
public LegendItemCollection getLegendItems() {
LegendItemCollection legendItemCollection = new LegendItemCollection();
for (Map.Entry<String, Color> entry : colourMap.entrySet()) {
legendItemCollection.add(new LegendItem(entry.getKey(), entry.getValue()));
}
return legendItemCollection;
}
@Override
public Paint getItemPaint(int row, int col) {
if (colours.isEmpty() || this.row != row || this.col != col) {
initInfoPerResource(row, col);
this.row = row;
this.col = col;
index = 0;
}
int colourIndex = index++ / PASS;
// System.out.println(colourIndex + "/" + colours.size());
return colours.get(colourIndex);
}
private void initInfoPerResource(int row, int col) {
colours.clear();
tooltips.clear();
Color c = (Color) super.getItemPaint(row, col);
float[] a = new float[3];
Color.RGBtoHSB(c.getRed(), c.getGreen(), c.getBlue(), a);
TaskSeries series = (TaskSeries) model.getRowKeys().get(row);
List<Task> tasks = series.getTasks();
Task resource = tasks.get(col);
int taskCount = resource.getSubtaskCount();
taskCount = Math.max(1, taskCount);
for (int i = 0; i < taskCount; i++) {
MySubtask subtask = (MySubtask)resource.getSubtask(i);
Color colour = colourMap.get(subtask.getItem().getActivityId());
colours.add(colour);
tooltips.add(subtask.getItem().getName());
}
}
}
@Override
public Class<? extends Serializable> getConfigClass() {
// TODO Auto-generated method stub
return null;
}
@Override
public JPanel createConfigEditorPanel(ComponentConfigurationEditorAPI api, int mode, Serializable config, boolean isFixedIO) {
// TODO Auto-generated method stub
return null;
}
@Override
public long getFlags(ODLApi api, int mode) {
return ODLComponent.FLAG_ALLOW_USER_INTERACTION_WHEN_RUNNING | ODLComponent.FLAG_OUTPUT_WINDOWS_CAN_BE_SYNCHRONISED;
}
@Override
public Icon getIcon(ODLApi api, int mode) {
return Icons.loadFromStandardPath("gantt.png");
}
@Override
public boolean isModeSupported(ODLApi api, int mode) {
return mode == ODLComponent.MODE_DEFAULT;
}
@Override
public void registerScriptTemplates(ScriptTemplatesBuilder templatesApi) {
templatesApi.registerTemplate("Gantt chart", "Gantt chart", "Gantt chart", new GanttChartComponent().getIODsDefinition(templatesApi.getApi(), null), (Serializable) null);
}
@Override
public String activityIdColumnName() {
return "activity-id";
}
@Override
public String resourceIdColumnName() {
return "resource-id";
}
@Override
public String startTimeColumnName() {
return "start-time";
}
@Override
public String endTimeColumnName() {
return "end-time";
}
@Override
public String colourSourceColumnName() {
return "colour";
}
@Override
public ODLDatastore<? extends ODLTableDefinition> getIODsDefinition() {
return getIODsDefinition(null, null);
}
}