/*******************************************************************************
* 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.jsprit;
import java.io.Serializable;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.components.ODLComponent;
import com.opendoorlogistics.api.components.PredefinedTags;
import com.opendoorlogistics.api.scripts.ScriptAdapter;
import com.opendoorlogistics.api.scripts.ScriptAdapterTable;
import com.opendoorlogistics.api.scripts.ScriptAdapterTable.ColumnSortType;
import com.opendoorlogistics.api.scripts.ScriptComponentConfig;
import com.opendoorlogistics.api.scripts.ScriptElement;
import com.opendoorlogistics.api.scripts.ScriptInputTables;
import com.opendoorlogistics.api.scripts.ScriptInstruction;
import com.opendoorlogistics.api.scripts.ScriptOption;
import com.opendoorlogistics.api.scripts.ScriptOption.OutputType;
import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder;
import com.opendoorlogistics.api.scripts.ScriptTemplatesBuilder.BuildScriptCallback;
import com.opendoorlogistics.api.standardcomponents.GanntChart;
import com.opendoorlogistics.api.standardcomponents.LineGraph;
import com.opendoorlogistics.api.standardcomponents.LineGraph.LGColumn;
import com.opendoorlogistics.api.standardcomponents.Maps;
import com.opendoorlogistics.api.standardcomponents.MatrixExporter;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor.EditorTable;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor.OrderField;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor.ResourceDescriptionField;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor.ResourceTypeField;
import com.opendoorlogistics.api.standardcomponents.ScheduleEditor.TaskField;
import com.opendoorlogistics.api.standardcomponents.TableCreator;
import com.opendoorlogistics.api.tables.ODLColumnType;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLTableDefinition;
import com.opendoorlogistics.api.tables.TableFlags;
import com.opendoorlogistics.components.jsprit.VRPBuilder.TravelCostType;
import com.opendoorlogistics.components.jsprit.tabledefinitions.InputTablesDfn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.OutputTablesDfn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.RouteDetailsTableDfn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.StopDetailsTableDfn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.StopsTableDefn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.VehiclesTableDfn;
import com.opendoorlogistics.components.jsprit.tabledefinitions.StopsTableDefn.StopType;
class VRPScriptWizard {
final private static String STOP_DETAILS_TABLE = "stop-details";
final private static String ROUTE_DETAILS_TABLE = "route-details";
final private static String HTML_HEADER = "<html><body style='width: 350 px'>";
final private ODLApi api;
private final VRPComponent component = new VRPComponent();
private final ScriptTemplatesBuilder templatesApi;
VRPScriptWizard(final ScriptTemplatesBuilder templatesApi) {
this.templatesApi = templatesApi;
this.api = templatesApi.getApi();
}
void registerScriptTemplates() {
final ODLApi api = templatesApi.getApi();
// VRPConfig conf = new VRPConfig();
for (int i = 0; i <= 5; i++) {
final int nq = i;
String shortName = "VRP";
String name = "Vehicle Routing";
String quant = " with " + nq + " quantity type(s)";
shortName += quant;
name += quant;
VRPConfig config = new VRPConfig();
config.setNbQuantities(nq);
ODLDatastore<? extends ODLTableDefinition> iods = new VRPComponent().getIODsDefinition(api, config);
templatesApi.registerTemplate(shortName, name, name, iods, new BuildScriptCallback() {
@Override
public void buildScript(ScriptOption builder) {
buildAll( nq, builder);
}
});
}
}
/**
* @param api
* @param detailsDsId
* @param inputTableName
* @param adapter
*/
private void addMapTables(ODLApi api, String inputDataAdapterId, String detailsDsId, String outputTableName, ScriptAdapter adapter, boolean isReport) {
ODLTableDefinition drawable = api.standardComponents().map().getDrawableTableDefinition();
// init lines adapter table
ScriptAdapterTable lines = adapter.addSourcelessTable(drawable);
lines.setSourceTable(detailsDsId, STOP_DETAILS_TABLE);
lines.setSourceColumns(
new String[][] { new String[] { PredefinedTags.LATITUDE, null },
new String[] { PredefinedTags.LONGITUDE, null }, new String[] { "geometry", PredefinedTags.INCOMING_PATH },
new String[] { "legendKey", PredefinedTags.VEHICLE_ID },
new String[] { "imageFormulaKey", PredefinedTags.VEHICLE_ID },
new String[] { "label", PredefinedTags.VEHICLE_ID },
new String[] { "labelGroupKey", PredefinedTags.VEHICLE_ID },
});
lines.setFormulae(new String[][] {
new String[] { "colour", "colourmultiply(randcolour(\"" + PredefinedTags.VEHICLE_ID + "\"),0.7)" },
new String[] { "pixelWidth", isReport ? "10" : "2" },
new String[] { "legendColour", "randcolour(\"" + PredefinedTags.VEHICLE_ID + "\")" },
});
lines.setTableFilterFormula("len(\"" + PredefinedTags.INCOMING_PATH + "\")>0");
if (outputTableName != null) {
lines.setTableName(outputTableName);
}
lines.setFlags(lines.getFlags() | TableFlags.FLAG_IS_DRAWABLES);
// init points adapter table
ScriptAdapterTable points = adapter.addSourcelessTable(drawable);
points.setSourceTable(detailsDsId, STOP_DETAILS_TABLE);
points.setSourceColumns(new String[][] { new String[] { PredefinedTags.LATITUDE, PredefinedTags.STOP_LATITUDE }, new String[] { PredefinedTags.LONGITUDE, PredefinedTags.STOP_LONGITUDE }, new String[] { "colourKey", null },
new String[] { "legendKey", PredefinedTags.VEHICLE_ID }, new String[] { "imageFormulaKey", PredefinedTags.VEHICLE_ID }, });
if (isReport) {
points.setFormula("fontSize", "50");
}
points.setFormulae(new String[][] {
new String[] { "label", "if(" + PredefinedTags.TYPE + "=\"" + VRPConstants.DEPOT + "\",\"depot\",\"stop-number\")" },
new String[] { "colour", "randcolour(\"" + PredefinedTags.VEHICLE_ID + "\")" },
new String[] { "drawOutline", "true" },
new String[] { "tooltip", "\"arrival-time\" & \" - \" & \"stop-name\" & \", \" & \"stop-address\"" },
});
if (outputTableName != null) {
points.setTableName(outputTableName);
}
// setup stops symbols
StringBuilder builder = new StringBuilder();
builder.append("switch(");
builder.append(PredefinedTags.TYPE);
builder.append(",\"" + VRPConstants.DEPOT + "\",\"fat-star\"");
builder.append(",\"" + StopType.LINKED_DELIVERY.getPrimaryCode() + "\",\"inverted-triangle\"");
builder.append(",\"" + StopType.UNLINKED_DELIVERY.getPrimaryCode() + "\",\"circle\"");
builder.append(",\"" + StopType.LINKED_PICKUP.getPrimaryCode() + "\",\"triangle\"");
builder.append(",\"" + StopType.UNLINKED_PICKUP.getPrimaryCode() + "\",\"square\"");
builder.append(")");
points.setFormula("symbol", builder.toString());
points.setFlags(points.getFlags() | TableFlags.FLAG_IS_DRAWABLES);
// setup stops sizes
builder = new StringBuilder();
builder.append("switch(");
builder.append(PredefinedTags.TYPE);
builder.append(",\"" + VRPConstants.DEPOT + "\"," + (isReport?"60":"30") );
builder.append(",\"" + StopType.LINKED_DELIVERY.getPrimaryCode()+ "\"," + (isReport?"36":"18") );
builder.append(",\"" + StopType.UNLINKED_DELIVERY.getPrimaryCode()+ "\"," + (isReport?"24":"12") );
builder.append(",\"" + StopType.LINKED_PICKUP.getPrimaryCode()+ "\"," + (isReport?"36":"18") );
builder.append(",\"" + StopType.UNLINKED_PICKUP.getPrimaryCode()+ "\"," + (isReport?"28":"14") );
builder.append(")");
points.setFormula("pixelWidth", builder.toString());
// init unassigned stops adapter
ScriptAdapterTable unassigned = adapter.addSourcelessTable(drawable);
unassigned.setTableFilterFormula("lookupcount(id,\"" + inputDataAdapterId + ", stop-order\",\"stop-id\")=0");
unassigned.setSourceTable(inputDataAdapterId, "Stops");
unassigned.setSourceColumns(new String[][] { new String[] { PredefinedTags.LATITUDE, PredefinedTags.LATITUDE }, new String[] { PredefinedTags.LONGITUDE, PredefinedTags.LONGITUDE } });
unassigned.setFormula("colour", "\"black\"");
if(isReport){
unassigned.setFormula("pixelWidth", "40");
}
else{
unassigned.setSourceColumn(PredefinedTags.LABEL, PredefinedTags.ID);
}
unassigned.setFormula("legendKey", "\"Unassigned\"");
unassigned.setFlags(points.getFlags() | TableFlags.FLAG_IS_DRAWABLES);
}
/**
* @param api
* @param builder
* @param detailsDsId
* @return
*/
private ScriptAdapter createShowMapAdapter(ODLApi api, ScriptOption builder, String inputDataAdapterId, String detailsDsId) {
ScriptAdapter adapter = builder.addDataAdapter("Map view");
// final String id = adapter.getAdapterId();
addMapTables(api, inputDataAdapterId, detailsDsId, null, adapter, false);
return adapter;
}
private void buildLoadGraph(ScriptOption showSolution , String detailsDsId, ODLApi api,VRPConfig config){
LineGraph lgc = api.standardComponents().lineGraph();
StopDetailsTableDfn sdfn = new OutputTablesDfn(api, config).stopDetails;
for(int i =0 ; i < config.getNbQuantities() ; i++){
String s = "View loads " + (i+1);
ScriptOption builder = showSolution.addOption(s,s);
builder.setSynced(true);
// set the data adapter
ScriptAdapter adapter = builder.addDataAdapter("Input to view loads " + (i+1));
for(int j=0; j <= 1 ; j++){
ScriptAdapterTable table = adapter.addSourcelessTable(lgc.getInputTableDefinition(api));
table.setSourceTable(detailsDsId, sdfn.table.getName());
table.setFormula(lgc.getColumnName(LGColumn.Key), "\""+PredefinedTags.VEHICLE_ID + "\"");
if(j==1){
table.setFormula(lgc.getColumnName(LGColumn.X), "decimalhours(\"" + PredefinedTags.LEAVE_TIME + "\")");
table.setSourceColumn(lgc.getColumnName(LGColumn.Y), sdfn.table.getColumnName(sdfn.leaveQuantities[i]));
}else{
table.setFormula(lgc.getColumnName(LGColumn.X), "decimalhours(\"" + PredefinedTags.ARRIVAL_TIME + "\")");
table.setSourceColumn(lgc.getColumnName(LGColumn.Y), sdfn.table.getColumnName(sdfn.arrivalQuantities[i]));
}
// set sorting...
int sortIndx=table.addColumn("SortField", ODLColumnType.STRING, true, "\""+PredefinedTags.VEHICLE_ID + "\"");
table.setSortType(sortIndx, ColumnSortType.ASCENDING);
}
// set the config
Serializable lgconfig = null;
try {
lgconfig = lgc.getConfigClass().newInstance();;
} catch (Exception e) {
throw new RuntimeException(e);
}
lgc.setXLabel("Decimal hours", lgconfig);
lgc.setYLabel("Quantity " + (i+1), lgconfig);
lgc.setTitle("Load on vehicles over time", lgconfig);
// create the instruction
builder.addInstruction(adapter.getAdapterId(), lgc.getId(), ODLComponent.MODE_DEFAULT, lgconfig);
}
}
private void buildGantt(ScriptOption builder, String detailsDsId, ODLApi api) {
builder.setSynced(true);
GanntChart ganntChart = api.standardComponents().ganttChart();
if (ganntChart != null) {
// create the adapter
ScriptAdapter adapter = builder.addDataAdapter("Gantt input");
ODLDatastore<? extends ODLTableDefinition> ds = ganntChart.getIODsDefinition();
String arrive = "\"" + PredefinedTags.ARRIVAL_TIME + "\"";
String leave = "\"" + PredefinedTags.LEAVE_TIME + "\"";
String wait = "\"" + PredefinedTags.WAITING_TIME + "\"";
String stopName ="\"" + PredefinedTags.STOP_NAME + "\"";
for (int i = 0; i < 3; i++) {
String activityType = "";
String colour = "";
String start = "";
String end = "";
String name = "";
switch (i) {
case 0:
activityType = "Travel";
colour = "colour(0,0,0.8)";
start = arrive + " - \"" + PredefinedTags.TRAVEL_TIME + "\"";
end = arrive;
name = "\"Travel to \"";
break;
case 1:
activityType = "Service";
colour = "colour(0,0.8,0)";
start = arrive + " + " + wait;
end = leave;
name = "\"Service \"";
break;
case 2:
activityType = "Wait";
colour = "colour(0.8,0,0)";
start = arrive;
end = arrive + " + " +wait;
name = "\"Wait at \"";
break;
}
name += " & " + stopName + " & \" from \" & round2second(" + start + ") & \" to \" & round2second(" + end + ")";
ScriptAdapterTable table = adapter.addSourcelessTable(ds.getTableAt(0));
table.setSourceTable(detailsDsId, STOP_DETAILS_TABLE);
table.setFormula(ganntChart.activityIdColumnName(), "\"" + activityType + "\"");
table.setSourceColumn(ganntChart.resourceIdColumnName(), PredefinedTags.VEHICLE_ID);
table.setFormula(ganntChart.colourSourceColumnName(), colour);
table.setFormula(ganntChart.startTimeColumnName(), start);
table.setFormula(ganntChart.endTimeColumnName(), end);
table.setFormula(PredefinedTags.NAME, name);
}
// add the instruction
builder.addInstruction(adapter.getAdapterId(), ganntChart.getId(), ODLComponent.MODE_DEFAULT).setEditorLabel(
HTML_HEADER + "This shows a Gantt chart where travel time, service time and waiting time are displayed in different colours.</html>");
}
}
private void buildReport(ScriptOption builder, String inputDataAdapterId, String detailsDsId, OutputTablesDfn outTables) {
// create adapter
ScriptAdapter adapter = builder.addDataAdapter("Report input");
builder.setEditorLabel(HTML_HEADER + "Run this generate route reports which can be exported to pdf, html etc.</html>");
class AddColumns {
void add(ODLTableDefinition dfn, int[] indices, int vehicleIdIndx, ScriptAdapterTable outTable) {
for (int index : indices) {
String name = dfn.getColumnName(index);
outTable.addColumn(name, dfn.getColumnType(index), false, name);
if (index == vehicleIdIndx) {
outTable.setColumnFlags(name, outTable.getColumnFlags(name) | TableFlags.FLAG_IS_REPORT_KEYFIELD);
}
}
}
}
AddColumns addColumns = new AddColumns();
// add header map, using separate adapter so we can reference it from formulae
ScriptAdapter mapAdapter = builder.addDataAdapter("Reporter map view");
String mapTableName = api.standardComponents().map().getDrawableTableDefinition().getName();
addMapTables(api, inputDataAdapterId, detailsDsId, mapTableName, mapAdapter, true);
ODLTableDefinition mapDfn = mapAdapter.getTable(0).getTableDefinition();
adapter.addSourcedTableToAdapter(mapAdapter.getAdapterId(), mapDfn, mapDfn).setTableName(api.standardComponents().reporter().getHeaderMapTableName());
// add routes table
ScriptAdapterTable routes = adapter.addEmptyTable("Routes");
routes.setSourceTable(detailsDsId, ROUTE_DETAILS_TABLE);
RouteDetailsTableDfn rdtdfn = outTables.routeDetails;
addColumns.add(rdtdfn.table, new int[] { rdtdfn.vehicleName, rdtdfn.vehicleId, rdtdfn.stopCount, rdtdfn.travelCosts[TravelCostType.DISTANCE_KM.ordinal()] }, rdtdfn.vehicleId, routes);
routes.addColumn("Picture", ODLColumnType.IMAGE, true, "printableimage(\"vehicle-id\", \"Reporter map view, " + mapTableName + "\", 1, 5 , 5, 200)");
// add stops table
StopDetailsTableDfn sdtdfn = outTables.stopDetails;
ScriptAdapterTable stops = adapter.addEmptyTable("Stops");
stops.setSourceTable(detailsDsId, STOP_DETAILS_TABLE);
addColumns.add(sdtdfn.table, new int[] { sdtdfn.stopNumber, sdtdfn.stopName, sdtdfn.stopAddress, sdtdfn.arrivalTime, sdtdfn.vehicleid, }, sdtdfn.vehicleid, stops);
stops.setTableFilterFormula("\"" + PredefinedTags.TYPE + "\"!=\"depot\"");
// create instruction
builder.addInstruction(adapter.getAdapterId(), builder.getApi().standardComponents().reporter().getId(), ODLComponent.MODE_DEFAULT);
}
/**
* @param api
* @param config
* @param inputDataAdapterId
* @param showSolution
*/
private void buildUnassignedStops(final ODLApi api, VRPConfig config, final String inputDataAdapterId, ScriptOption showSolution) {
String name = "View unassigned stops";
ScriptOption viewTable = showSolution.addOption(name, name);
viewTable.setEditorLabel(HTML_HEADER + "Run this option to view stops not loaded onto any route.</html>");
ScriptAdapter vtAdapter = viewTable.addDataAdapter("Unassigned stops data adapter");
ODLTableDefinition stopsTable = new InputTablesDfn(api, config).stops.table;
ScriptAdapterTable unassigedTable = vtAdapter.addSourcedTableToAdapter(inputDataAdapterId, stopsTable, stopsTable);
unassigedTable.setTableName("Unassigned stops");
unassigedTable.setTableFilterFormula("lookupcount(id,\"" + inputDataAdapterId + ", stop-order\",\"stop-id\")=0");
viewTable.addInstruction(vtAdapter.getAdapterId(), api.standardComponents().tableViewer().getId(), ODLComponent.MODE_DEFAULT);
viewTable.setSynced(true);
}
/**
* @param api
* @param nq
* @param builder
*/
private void buildAll( final int nq, ScriptOption builder) {
builder.setEditorLabel(HTML_HEADER
+ "The vehicle routing module uses the jsprit vehicle routing library. It can model standard vehicle routing problems and pickup-deliver vehicle routing problems with or without time windows. Any number of capacity contraints can be set.</html>");
// build an adapter for the common io
VRPConfig config = new VRPConfig();
config.setNbQuantities(nq);
ODLDatastore<? extends ODLTableDefinition> iods = component.getIODsDefinition(api, config);
// add the adapter at the script's root level
ScriptAdapter adptBuilder = builder.addDataAdapter("Input data");
final String inputDataAdapterId = adptBuilder.getAdapterId();
ScriptInputTables inputTables = builder.getInputTables();
for (int i = 0; i < iods.getTableCount(); i++) {
if (inputTables != null && i < inputTables.size() && inputTables.getSourceTable(i) != null) {
// use input table if we have one
adptBuilder.addSourcedTableToAdapter(inputTables.getSourceDatastoreId(i), inputTables.getSourceTable(i), iods.getTableAt(i));
} else {
// otherwise don't match to a source and just give a default source pointing to an identical table in the spreadsheet
adptBuilder.addSourcedTableToAdapter(api.stringConventions().getSpreadsheetAdapterId(), iods.getTableAt(i), iods.getTableAt(i));
}
}
adptBuilder.setEditorLabel("<html><h3>Data description</h3>The vehicle routing model requires three tables - <em>stops</em>, <em>vehicles</em> and <em>stop-order</em>."
+ "<ol><li><em>Stops</em> contains the individual locations which must be visited.</li>" + "<li><em>Vehicle-Types</em> which contains the types of vehicles available to serve the stops.</li>"
+ "<li>The generated vehicle routes are then saved to the <em>stops-order table.</em></li></ol></html>");
// create shared stand-alone config
ScriptComponentConfig settings = builder.addComponentConfig("Settings", VRPConstants.COMPONENT_ID, config);
// add the optimise option
buildOptimise(builder, inputDataAdapterId, settings);
// // add edit routes option and hook this up to the main adapter
// if(!VRPConstants.ROUTE_EDITING_SHOWS_STATS){
// buildEditRoutes(new InputTablesDfn(api, config),inputDataAdapterId,null, builder);
// }
// create show solution option
buildShowSolution(builder, config, inputDataAdapterId, settings);
// add the tools option
buildTools(builder, config, inputDataAdapterId, settings);
}
/**
* @param builder
* @param config
* @param inputDataAdapterId
* @param settings
*/
private void buildTools(ScriptOption builder, VRPConfig config, final String inputDataAdapterId, ScriptComponentConfig settings) {
ScriptOption tools = builder.addOption("Tools", "Tools");
// create all empty tables
String optionName = "Create all input tables";
InputTablesDfn inputTablesDfn = new InputTablesDfn(api, config);
ODLTableDefinition[]inputTables = new ODLTableDefinition[inputTablesDfn.ds.getTableCount()];
for(int i =0 ; i< inputTablesDfn.ds.getTableCount();i++){
inputTables[i] = inputTablesDfn.ds.getTableAt(i);
}
TableCreator tableCreatorComponent =api.standardComponents().tableCreator();
Serializable tableCreatorConfig = tableCreatorComponent.createConfiguration(api, inputTables);
ScriptOption addOption = tools.addOption(optionName, optionName);
ScriptInstruction addInstruction = addOption.addInstruction(null, tableCreatorComponent.getId(), ODLComponent.MODE_DEFAULT, tableCreatorConfig);
addOption.addCopyTable(addInstruction.getOutputDatastoreId(), "", OutputType.COPY_ALL_TABLES, "");
// create empty tables one-by-one
for(int i =0 ; i< inputTablesDfn.ds.getTableCount();i++){
ODLTableDefinition table = inputTablesDfn.ds.getTableAt(i);
optionName = "Create " + table.getName().toLowerCase() + " table";
tableCreatorConfig = tableCreatorComponent.createConfiguration(api, table);
addOption = tools.addOption(optionName, optionName);
addInstruction = addOption.addInstruction(null, tableCreatorComponent.getId(), ODLComponent.MODE_DEFAULT, tableCreatorConfig);
addOption.addCopyTable(addInstruction.getOutputDatastoreId(), table.getName(), OutputType.COPY_TO_NEW_TABLE, table.getName());
}
// create option to fill with demo data
String demoName = "Demo - create stops and vehicles";
tools.addOption(demoName, demoName).addInstruction(inputDataAdapterId, VRPConstants.COMPONENT_ID, VRPConstants.BUILD_DEMO_MODE, settings.getComponentConfigId());
tools.addOption("Validate data relations", "Validate data relations").addInstruction(inputDataAdapterId, VRPConstants.COMPONENT_ID, ODLComponent.MODE_DATA_UPDATER, settings.getComponentConfigId());
buildMatrixExporter(new InputTablesDfn(api, config), inputDataAdapterId, tools);
}
/**
* @param builder
* @param config
* @param inputDataAdapterId
* @param settings
*/
private void buildShowSolution(ScriptOption builder, VRPConfig config, final String inputDataAdapterId, ScriptComponentConfig settings) {
ScriptOption showSolution = builder.addOption("View solution", "View solution");
showSolution.setEditorLabel(HTML_HEADER + "This option generates statistics for your routes - e.g. total distance, total time, arrival time at stops etc.</html>");
ScriptInstruction instructionBuilder = showSolution.addInstruction(inputDataAdapterId, VRPConstants.COMPONENT_ID, VRPConstants.SOLUTION_DETAILS_MODE, settings.getComponentConfigId());
instructionBuilder.setName("Generate solution details");
String detailsDsId = builder.createUniqueDatastoreId("Solution details");
instructionBuilder.setOutputDatastoreId(detailsDsId);
// if(VRPConstants.ROUTE_EDITING_SHOWS_STATS){
buildEditRoutes(new InputTablesDfn(api, config),inputDataAdapterId,detailsDsId, showSolution);
// }
// create show map
buildShowMap(inputDataAdapterId, detailsDsId, showSolution);
// create show tables
OutputTablesDfn outTables = buildShowSolutionTables(config, detailsDsId, showSolution);
// Create unassigned stops table
buildUnassignedStops(api, config, inputDataAdapterId, showSolution);
// create reports
buildReport(showSolution.addOption("Reports", "Reports"), inputDataAdapterId, detailsDsId, outTables);
// create gantt chart
buildGantt(showSolution.addOption("Gantt chart", "Gantt chart"), detailsDsId, api);
// create load graph(s)
buildLoadGraph(showSolution, detailsDsId, api, config);
// add export options
buildExportTables(showSolution.addOption("Export solution tables", "Export solution tables"), detailsDsId, outTables);
}
/**
* @param builder
* @param inputDataAdapterId
* @param settings
*/
private void buildOptimise(ScriptOption builder, final String inputDataAdapterId, ScriptComponentConfig settings) {
ScriptOption optOption = builder.addOption("Optimise", "Optimise");
optOption.setEditorLabel(HTML_HEADER + "Run this option to create a new set of optimised vehicle routes. Pre-existing vehicle routes will be deleted.</html>");
ScriptElement instrBuilder = optOption.addInstruction(inputDataAdapterId, VRPConstants.COMPONENT_ID, ODLComponent.MODE_DEFAULT, settings.getComponentConfigId());
instrBuilder.setName("Optimise routes");
}
private void buildMatrixExporter(InputTablesDfn inputDfn,final String inputDataAdapterId, ScriptOption builder){
ScriptOption option = builder.addOption("Export matrix", "Export travel matrix");
option.setEditorLabel(HTML_HEADER + "Run this option to export a matrix of travel distances and times between all locations to a text file. "
+ "<b>Warning</b> - the matrix exporter has its own settings for distance generation, independent of the main distances settings. "
+ "If you want to export the exact same matrix as the optimiser is using, ensure your matrix exporter distance settings are identical to the main ones.</html>");
ScriptAdapter ungrouped = option.addDataAdapter("1. Export matrix input data (ungrouped)");
ScriptAdapter grouped = option.addDataAdapter("2. Export matrix input data (grouped)");
MatrixExporter component = api.standardComponents().matrixExporter();
ScriptInstruction instruction = option.addInstruction(grouped.getAdapterId(),component.getId(), ODLComponent.MODE_DEFAULT, component.createConfig(false));
instruction.setName("Export matrix");
ODLTableDefinition inputTable= instruction.getInstructionRequiredIO().getTableAt(0);
// create the ungroup adapters
ScriptAdapterTable fromStops=ungrouped.addSourcelessTable(inputTable);
fromStops.setSourceTable(inputDataAdapterId, StopsTableDefn.STOPS_TABLE_NAME);
fromStops.setSourceColumn(PredefinedTags.LATITUDE, PredefinedTags.LATITUDE);
fromStops.setSourceColumn(PredefinedTags.LONGITUDE, PredefinedTags.LONGITUDE);
for(int i =0 ; i<=1 ; i++){
String prefix = i==0?"start-" : "end-";
ScriptAdapterTable vehicleEnd = ungrouped.addSourcelessTable(inputTable);
vehicleEnd.setSourceTable(inputDataAdapterId, VehiclesTableDfn.VEHICLE_TYPES_TABLE_NAME);
vehicleEnd.setSourceColumn(PredefinedTags.LATITUDE, prefix + PredefinedTags.LATITUDE);
vehicleEnd.setSourceColumn(PredefinedTags.LONGITUDE, prefix + PredefinedTags.LONGITUDE);
}
// now group them
ScriptAdapterTable groupedTable = grouped.addSourcedTableToAdapter(ungrouped.getAdapterId(), inputTable, inputTable);
for(String col : new String[]{PredefinedTags.LATITUDE,PredefinedTags.LONGITUDE}){
groupedTable.setColumnFlags(col, groupedTable.getColumnFlags(col)| TableFlags.FLAG_IS_GROUP_BY_FIELD);
}
}
/**
* @param inputDataAdapterId
* @param builder
*/
private void buildEditRoutes(InputTablesDfn inputDfn,final String inputDataAdapterId,String detailsDs, ScriptOption builder) {
ScriptOption editRoutes = builder.addOption("Edit routes", "Edit routes");
editRoutes.setEditorLabel(HTML_HEADER + "Run this option to edit the vehicle routes. You can drag-and-drop stops between different routes.</html>");
final ScriptAdapter erAdapter = editRoutes.addDataAdapter("Input data (editor)");
String erAdptId = erAdapter.getAdapterId(); // builder.createUniqueAdapterId("Input data (editor)");
final ScheduleEditor component = api.standardComponents().scheduleEditor();
ScriptInstruction instruction = editRoutes.addInstruction(erAdptId,component.getId(), ODLComponent.MODE_DEFAULT);
instruction.setName("Edit routes");
final ODLDatastore<? extends ODLTableDefinition> iods = instruction.getInstructionRequiredIO();
class Helper{
ScriptAdapterTable createAdapterTable(EditorTable et){
return erAdapter.addSourcelessTable(api.tables().findTable(iods, component.getTableName(et)));
}
void setSourceColumn(EditorTable et, Enum<?> field, String sourceName, ScriptAdapterTable adapterTable){
adapterTable.setSourceColumn(component.getFieldName(et, field), sourceName);
}
void setFormula(EditorTable et, Enum<?> field, String function, ScriptAdapterTable adapterTable){
adapterTable.setFormula(component.getFieldName(et, field), function);
}
}
Helper helper = new Helper();
// configure stops table
ScriptAdapterTable stops=helper.createAdapterTable(EditorTable.TASKS);
stops.setSourceTable(inputDataAdapterId, inputDfn.stops.table.getName());
helper.setSourceColumn(EditorTable.TASKS, TaskField.ID, PredefinedTags.ID, stops);
helper.setSourceColumn(EditorTable.TASKS, TaskField.NAME, PredefinedTags.NAME, stops);
stops.addColumn("Type", ODLColumnType.STRING, false, PredefinedTags.TYPE);
stops.addColumn("Address", ODLColumnType.STRING, false, PredefinedTags.ADDRESS);
// configure vehicle types table
ScriptAdapterTable vehicles=helper.createAdapterTable(EditorTable.RESOURCE_TYPES);
vehicles.setSourceTable(inputDataAdapterId, inputDfn.vehicles.table.getName());
helper.setSourceColumn(EditorTable.RESOURCE_TYPES, ResourceTypeField.ID, PredefinedTags.VEHICLE_ID, vehicles);
helper.setSourceColumn(EditorTable.RESOURCE_TYPES, ResourceTypeField.NAME, PredefinedTags.VEHICLE_NAME, vehicles);
helper.setSourceColumn(EditorTable.RESOURCE_TYPES, ResourceTypeField.RESOURCE_COUNT, PredefinedTags.NUMBER_OF_VEHICLES, vehicles);
// configure order table
ScriptAdapterTable order=helper.createAdapterTable(EditorTable.TASK_ORDER);
order.setSourceTable(inputDataAdapterId, inputDfn.stopOrder.table.getName());
helper.setSourceColumn(EditorTable.TASK_ORDER, OrderField.RESOURCE_ID, PredefinedTags.VEHICLE_ID, order);
helper.setSourceColumn(EditorTable.TASK_ORDER, OrderField.TASK_ID, PredefinedTags.STOP_ID, order);
helper.setFormula(EditorTable.TASK_ORDER, OrderField.COLOUR, "if( lookup(\"stop-id\",\"Solution details,stop-details\", \"stop-id\",\"has-violation\") ,color(1,0.9,0.9) ,null)", order);
order.addColumn("Type", ODLColumnType.STRING, true, "lookup(\"stop-id\", \"stops\", \"id\",\"type\")");
order.addColumn("Address", ODLColumnType.STRING, true, "lookup(\"stop-id\", \"stops\", \"id\",\"address\")");
if(detailsDs!=null){
String tableRef ="\"" + detailsDs + ",stop-details" + "\"";
order.addColumn("Arrival time", ODLColumnType.STRING, true, "lookup(\"stop-id\"," + tableRef + ", \"stop-id\",\"arrival-time\")");
}
// configure resource description table
ScriptAdapterTable description = helper.createAdapterTable(EditorTable.RESOURCE_DESCRIPTIONS);
description.setSourceTable(detailsDs, ROUTE_DETAILS_TABLE);
helper.setSourceColumn(EditorTable.RESOURCE_DESCRIPTIONS, ResourceDescriptionField.RESOURCE_ID, PredefinedTags.VEHICLE_ID, description);
StringBuilder descText = new StringBuilder();
descText.append("\"<html>\"");
int nq = inputDfn.stops.quantityIndices.length;
for(int i =0 ; i<nq ; i++){
// "<html><b>Quantity</b>: " & "delivered-quantity" & "/" & "capacity"
if(i>0){
descText.append(" & \"  \"");
}
String sIndx = nq>1 ? Integer.toString(i+1):"";
descText.append(" & \"<b>Quantity" + sIndx + "</b>:\"");
descText.append(" & \"delivered-quantity" + sIndx + "\"");
descText.append(" & \"/\"");
descText.append(" & \"capacity" + sIndx + "\"");
}
//"<br/><b>Start-time</b>: " & "start-time" & "   <b>End-time</b>: " & "end-time" &
if(nq>0){
descText.append(" & \"<br/>\"");
}
descText.append(" & \"<b>Start-time</b>: \" & \"start-time\" & \"   <b>End-time</b>: \" & \"end-time\" & \"   <b>TW violation</b>: \" & \"time-window-violation\"" );
//"<br/><b>Travel-km</b>: " & decimalformat("0.000","travel-km") & "   <b>Travel-time</b>: " & "travel-time" & "</html>"
descText.append(" & \"<br/><b>Travel-km</b>: \" & decimalformat(\"0.000\",\"travel-km\") & \"   <b>Travel-time</b>: \" & \"travel-time\" & \"</html>\"");
helper.setFormula(EditorTable.RESOURCE_DESCRIPTIONS, ResourceDescriptionField.DESCRIPTION, descText.toString(), description);
}
/**
* @param inputDataAdapterId
* @param detailsDsId
* @param showSolution
*/
private void buildShowMap(final String inputDataAdapterId, String detailsDsId, ScriptOption showSolution) {
ScriptOption showMap = showSolution.addOption("View routes in map", "View routes in map");
showMap.setEditorLabel(HTML_HEADER + "Run this option to view the vehicle routes in a map.</html>");
ScriptAdapter showMapAdapter = createShowMapAdapter(api, showMap, inputDataAdapterId, detailsDsId);
Maps mapComponent = api.standardComponents().map();
Serializable config = null;
try{
config = mapComponent.getConfigClass().newInstance();
mapComponent.setCustomTooltips(true, config);
}catch(Exception e){
throw new RuntimeException(e);
}
showMap.addInstruction(showMapAdapter.getAdapterId(),mapComponent.getId(), ODLComponent.MODE_DEFAULT, config);
showMap.setSynced(true);
}
/**
* @param config
* @param detailsDsId
* @param showSolution
* @return
*/
private OutputTablesDfn buildShowSolutionTables(VRPConfig config, String detailsDsId, ScriptOption showSolution) {
OutputTablesDfn outTables = new OutputTablesDfn(api, config);
for (int i = 0; i < outTables.ds.getTableCount(); i++) {
// add the option
ODLTableDefinition table = outTables.ds.getTableAt(i);
String name = "View " + table.getName().toLowerCase();
ScriptOption viewTable = showSolution.addOption(name, name);
viewTable.setEditorLabel(HTML_HEADER + "Run this option to view the table " + table.getName().toLowerCase() + "</html>");
// add the adapter
ScriptAdapter vtAdapter = viewTable.addDataAdapter("Adapter for table " + table.getName().toLowerCase());
ScriptAdapterTable tableAdapter = vtAdapter.addSourcedTableToAdapter(detailsDsId, table, table);
removeGeometryFieldsFromAdapter(tableAdapter);
// add font colour violation...
tableAdapter.addColumn(PredefinedTags.ROW_FONT_COLOUR, ODLColumnType.COLOUR, true, "if(\"has-violation\", color(0.75,0,0), null)");
viewTable.addInstruction(vtAdapter.getAdapterId(), api.standardComponents().tableViewer().getId(), ODLComponent.MODE_DEFAULT);
viewTable.setSynced(true);
}
return outTables;
}
private void removeGeometryFieldsFromAdapter(ScriptAdapterTable tableAdapter) {
// remove geometry fields as they're too long to show
int col=0;
while(col < tableAdapter.getColumnCount()){
if(tableAdapter.getColumnType(col)==ODLColumnType.GEOM){
tableAdapter.removeColumn(col);
}else{
col++;
}
}
}
private void buildExportTables(ScriptOption builder,final String detailsDsId, OutputTablesDfn outTables) {
// add the adapter, which removes the geometry fields
final ScriptAdapter adapter = builder.addDataAdapter("Table export data adapter");
for(int i =0 ; i < outTables.ds.getTableCount() ; i++){
ODLTableDefinition table = outTables.ds.getTableAt(i);
ScriptAdapterTable tableAdapter = adapter.addSourcedTableToAdapter(detailsDsId, table, table);
removeGeometryFieldsFromAdapter(tableAdapter);
}
class AddCopy{
void addCopy(ScriptOption option, ODLTableDefinition table){
option.addCopyTable(adapter.getAdapterId(), table.getName(), OutputType.REPLACE_CONTENTS_OF_EXISTING_TABLE, "Exported-" + table.getName());
}
}
AddCopy addCopy = new AddCopy();
// add option for each
for(int i =0 ; i < outTables.ds.getTableCount() ; i++){
ODLTableDefinition table = outTables.ds.getTableAt(i);
String optionName = "Export " + table.getName().toLowerCase();
ScriptOption singleExport = builder.addOption(optionName,optionName);
addCopy.addCopy(singleExport, table);
}
// and option to export all
String exportAllTitle ="Export all";
ScriptOption exportAll = builder.addOption(exportAllTitle,exportAllTitle);
for(int i =0 ; i < outTables.ds.getTableCount() ; i++){
ODLTableDefinition table = outTables.ds.getTableAt(i);
addCopy.addCopy(exportAll, table);
}
}
}