/*******************************************************************************
* 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.scheduleeditor;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.beans.PropertyVetoException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import javax.swing.DefaultListCellRenderer;
import javax.swing.JDesktopPane;
import javax.swing.JInternalFrame;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JSplitPane;
import javax.swing.JToolBar;
import javax.swing.SwingConstants;
import javax.swing.border.EtchedBorder;
import com.opendoorlogistics.api.ODLApi;
import com.opendoorlogistics.api.components.ComponentControlLauncherApi;
import com.opendoorlogistics.api.tables.ODLDatastore;
import com.opendoorlogistics.api.tables.ODLTable;
import com.opendoorlogistics.api.tables.TableFlags;
import com.opendoorlogistics.api.ui.Disposable;
import com.opendoorlogistics.codefromweb.TileInternalFrames;
import com.opendoorlogistics.components.scheduleeditor.data.AbstractResource;
import com.opendoorlogistics.components.scheduleeditor.data.DataProvider;
import com.opendoorlogistics.components.scheduleeditor.data.EditorData;
import com.opendoorlogistics.components.scheduleeditor.data.beans.Task;
import com.opendoorlogistics.components.scheduleeditor.data.beans.TaskOrder;
import com.opendoorlogistics.core.tables.beans.BeanMapping.BeanTableMappingImpl;
import com.opendoorlogistics.core.utils.strings.StandardisedStringSet;
import com.opendoorlogistics.core.utils.strings.Strings;
import com.opendoorlogistics.utils.ui.Icons;
import com.opendoorlogistics.utils.ui.ODLAction;
public class SchedulesEditorPanel extends JPanel implements TaskMover, Disposable, DataProvider {
private final ResourcesList vehiclesList = new ResourcesList(this);
// private final JDesktopPane desktopPane;
private ComponentControlLauncherApi api;
private ODLDatastore<? extends ODLTable> ioDs;
private EditorData data;
private ArrayList<Task> notLoadsOrder = new ArrayList<>();
public SchedulesEditorPanel(ComponentControlLauncherApi api) {
// routes panel
this.api = api;
// JPanel vehiclesPanel = new JPanel();
setLayout(new BorderLayout());
JLabel vehiclesLabel = new JLabel("Vehicles");
vehiclesLabel.setHorizontalAlignment(SwingConstants.CENTER);
add(vehiclesLabel, BorderLayout.NORTH);
add(new JScrollPane(vehiclesList), BorderLayout.CENTER);
vehiclesList.setBorder(new EtchedBorder(EtchedBorder.LOWERED, null, null));
// do a custom renderer for the list of vehicle names
vehiclesList.setCellRenderer(new DefaultListCellRenderer() {
@Override
public Component getListCellRendererComponent(JList<?> list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
AbstractResource vehicle = (AbstractResource)value;
boolean unloaded = Strings.equalsStd(vehicle.getId(), ScheduleEditorConstants.UNLOADED_VEHICLE);
int count = getStopsByVehicle(vehicle.getId()).length;
setText(vehicle.getId() + " (" + count + ")");
if(unloaded && !isSelected){
// red if we have unassigned stops, green if not
if(count>0){
setBackground(new Color(255, 200, 200));
}else{
setBackground(new Color(200, 255, 200));
}
}
return this;
}
});
// listener for clicking on routes panel
vehiclesList.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent evt) {
if (evt.getClickCount() == 2) {
int index = vehiclesList.locationToIndex(evt.getPoint());
if (index != -1) {
launchSingleScheduleEditor(data.getResources()[index]);
}
}
}
});
// add components to the panel
// desktopPane = new JDesktopPane();
// JPanel rightFrame = new JPanel();
//rightFrame.setLayout(new BorderLayout());
// don't use desktop scroll pane as tiling gets the incorrect size...
// rightFrame.add( desktopPane, BorderLayout.CENTER);
// JSplitPane splitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, vehiclesPanel,rightFrame);
// setLayout(new BorderLayout());
// add(splitPane, BorderLayout.CENTER);
// create toolbar on the right frame
JToolBar toolBar = new JToolBar();
toolBar.setLayout(new FlowLayout(FlowLayout.RIGHT));
toolBar.setFloatable(false);
add(toolBar, BorderLayout.SOUTH);
// rightFrame.add(toolBar, BorderLayout.SOUTH);
// toolBar.add(new ODLAction("Open all", "Open all routes", Icons.loadFromStandardPath("open-16x16.png")) {
//
// @Override
// public void actionPerformed(ActionEvent e) {
// closeSingleScheduleWindows();
//
// // open all.. doing in reverse order gets the same order as the list on-screen when tiled
// for(int i =vehiclesList.getModel().getSize()-1; i>=0 ;i--){
// launchSingleScheduleEditor(vehiclesList.getModel().getElementAt(i));
// }
//
// tileRoutes();
//
// }
// });
// toolBar.add(new ODLAction("Tile", "Tile all open route windows",Icons.loadFromStandardPath("application-tile-horizontal-16x16.png")) {
//
// @Override
// public void actionPerformed(ActionEvent e) {
// tileRoutes();
// }
// });
toolBar.add(new ODLAction("Close windows", "Close all route windows", Icons.loadFromStandardPath("close-all-windows.png")) {
@Override
public void actionPerformed(ActionEvent e) {
closeSingleScheduleWindows();
}
});
// allowing unloading by dropping onto the desktop pane
DropTarget dropTarget = new DropTarget();
dropTarget.setActive(true);
//desktopPane.setDropTarget(dropTarget);
try {
dropTarget.addDropTargetListener(new DropTargetListener() {
@Override
public void dropActionChanged(DropTargetDragEvent dtde) {
// TODO Auto-generated method stub
}
@Override
public void drop(DropTargetDropEvent dtde) {
Transferable transferable = dtde.getTransferable();
if(transferable!=null){
String []stopIds = TaskTransferHandler.getStopIds(transferable);
if(stopIds!=null){
moveStop(stopIds, ScheduleEditorConstants.UNLOADED_VEHICLE, 0);
}
}
}
@Override
public void dragOver(DropTargetDragEvent dtde) {
// TODO Auto-generated method stub
}
@Override
public void dragExit(DropTargetEvent dte) {
// TODO Auto-generated method stub
}
@Override
public void dragEnter(DropTargetDragEvent dtde) {
// TODO Auto-generated method stub
}
});
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void setData(ComponentControlLauncherApi api,ODLDatastore<? extends ODLTable> ioDs) {
this.api = api;
this.ioDs = ioDs;
this.data = EditorData.read(api.getApi(),ioDs);
// set vehicles
this.vehiclesList.setListData(data.getResources());
// Update not-loads 1 - remove any not loads no longer unloaded...
Task [] newNotLoads = data.getTasksByResource(ScheduleEditorConstants.UNLOADED_VEHICLE);
StandardisedStringSet newNotLoadsIds = Task.toTaskIds(Arrays.asList(newNotLoads));
Iterator<Task> it = notLoadsOrder.iterator();
while(it.hasNext()){
Task next = it.next();
if(newNotLoadsIds.contains(next.getId())==false){
it.remove();
}
}
// Update not-loads 2 - add any new not-loads at the front of the queue.
// Preserve order by doing this in reverse order.
StandardisedStringSet currentNotLoadsIds = Task.toTaskIds(notLoadsOrder);
for(int i = newNotLoads.length-1; i>=0;i--){
Task notLoad=newNotLoads[i];
if(currentNotLoadsIds.contains(notLoad.getId())==false){
notLoadsOrder.add(0, notLoad);
}
}
// Update open windows; if vehicle no longer exists they will close themselves
for(JPanel frame : api.getRegisteredPanels()){
if (SingleScheduleFrame.class.isInstance(frame)) {
SingleScheduleFrame sre = (SingleScheduleFrame)frame;
AbstractResource vehicle = data.getResource(sre.getVehicleId());
if(vehicle==null){
api.disposeRegisteredPanel(sre);
}else{
sre.setData(data);
api.setTitle(frame, sre.getTitle());
}
}
}
}
private Task [] getStopsByVehicle(String vehicleId){
if(Strings.equalsStd(ScheduleEditorConstants.UNLOADED_VEHICLE, vehicleId)){
return notLoadsOrder.toArray(new Task[notLoadsOrder.size()]);
}else{
return data.getTasksByResource(vehicleId);
}
}
private SingleScheduleFrame launchSingleScheduleEditor(AbstractResource vehicle) {
// close if already exists
for(JPanel frame : api.getRegisteredPanels()){
if (SingleScheduleFrame.class.isInstance(frame)) {
SingleScheduleFrame sre = (SingleScheduleFrame)frame;
if (Strings.equalsStd(sre.getVehicleId(), vehicle.getId())) {
api.disposeRegisteredPanel(frame);
}
}
}
SingleScheduleFrame sre = new SingleScheduleFrame(vehicle.getId(), this,api.getApi());
sre.setData(data);
// register panel setting it *not* refreshable as the main panel controls the refresh
api.registerPanel("SSF" + vehicle.getId(), sre.getTitle(), sre, false);
// try {
// sre.setMaximum(true);
// } catch (PropertyVetoException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
return sre;
}
// public static void main(String[] args) {
// try {
// // set look and feel correctly
// for (LookAndFeelInfo info : UIManager.getInstalledLookAndFeels()) {
// if ("Nimbus".equals(info.getName())) {
// UIManager.setLookAndFeel(info.getClassName());
// }
// }
// } catch (Exception e) {
// // TODO: handle exception
// }
//
// // create random data
// Random random = new Random();
// final VehicleType[] vehicleTypes = new VehicleType[2];
// int nbPerType=2;
// for (int i = 0; i < vehicleTypes.length; i++) {
// VehicleType vehicle = new VehicleType();
// String id = i==0? "Lorry" : "Van";
// vehicle.setId(id);
// vehicle.setName(id);
// vehicle.setNumber(2);
// vehicleTypes[i] = vehicle;
// }
//
// final EditorStop[] stops = new EditorStop[25];
// for (int i = 0; i < stops.length; i++) {
// stops[i] = new EditorStop();
// stops[i].setId("Stop" + (i + 1));
// stops[i].setName(ExampleData.getRandomBusinessName(random));
// }
//
// final EditorStopOrder[] stopOrder = new EditorStopOrder[stops.length / 2];
// for (int i = 0; i < stopOrder.length; i++) {
// stopOrder[i] = new EditorStopOrder();
// stopOrder[i].setVehicleId(vehicleTypes[random.nextInt(vehicleTypes.length)].getId() + (1+random.nextInt(nbPerType)));
// stopOrder[i].setStopId(stops[2 * i].getId());
// }
// Arrays.sort(stopOrder, new Comparator<EditorStopOrder>() {
//
// @Override
// public int compare(EditorStopOrder o1, EditorStopOrder o2) {
// return o1.getVehicleId().compareTo(o2.getVehicleId());
// }
// });
//
// // map to datastore
// class MapToDs{
// ODLDatastore<? extends ODLTable> doMapping(){
// // copy data into arrays
// BeanMappedRow[][] rows = new BeanMappedRow[3][];
// rows[RouteEditorConstants.VEHICLES_TABLE_INDEX] = new BeanMappedRow[vehicleTypes.length];
// System.arraycopy(vehicleTypes, 0, rows[RouteEditorConstants.VEHICLES_TABLE_INDEX], 0, vehicleTypes.length);
//
// rows[RouteEditorConstants.STOPS_TABLE_INDEX] = new BeanMappedRow[stops.length];
// System.arraycopy(stops, 0, rows[RouteEditorConstants.STOPS_TABLE_INDEX], 0, stops.length);
//
// rows[RouteEditorConstants.STOP_ORDER_TABLE_INDEX] = new BeanMappedRow[stopOrder.length];
// System.arraycopy(stopOrder, 0, rows[RouteEditorConstants.STOP_ORDER_TABLE_INDEX], 0, stopOrder.length);
//
// RouteEditorComponent component = new RouteEditorComponent();
// BeanDatastoreMapping beanMapping = component.getBeanMapping();
// ODLDatastore<? extends ODLTable> iods = beanMapping.writeObjectsToDatastore(rows);
// return iods;
// }
// }
// ODLDatastore<? extends ODLTable> iods = new MapToDs().doMapping();
//
// UndoRedoDecorator<ODLTable> undoRedo = new UndoRedoDecorator<>(ODLTable.class, iods);
// System.out.println(iods);
//
// ODLApiImpl api = new ODLApiImpl();
// RouteEditorPanel panel = new RouteEditorPanel(api);
// panel.setData(undoRedo);
// ShowPanel.showPanel(panel);
// }
@Override
public boolean moveStop(String[] stopIds, String vehicleId, int position) {
// check the stops are known...
if(stopIds.length==0){
return false;
}
for(String id:stopIds){
if(data.getTask(id)==null){
return false;
}
}
ODLTable stopsOrder = ioDs.getTableAt(ScheduleEditorConstants.TASK_ORDER_TABLE_INDEX);
boolean rollbackSupported = ioDs.isRollbackSupported();
long tableFlags = stopsOrder.getFlags();
boolean flagsOk = ((tableFlags & TableFlags.UI_SET_INSERT_DELETE_PERMISSION_FLAGS) == TableFlags.UI_SET_INSERT_DELETE_PERMISSION_FLAGS);
if (!rollbackSupported|| !flagsOk) {
showMessage("Cannot move stops. Route editing is unsupported on tables which have are filtered, sorted etc...");
return false;
}
// get the vehicle record
AbstractResource vehicle = data.getResource(vehicleId);
if (vehicle == null) {
return false;
}
// get the set of stop ids to be inserted
StandardisedStringSet movedSet = new StandardisedStringSet(false);
for (String stopId : stopIds) {
movedSet.add(stopId);
}
// get the current stops order on the vehicle
Task[] currentRoute =getStopsByVehicle(vehicleId);
// find the stops before and after the insertion position which aren't being moved themselves
Task stopBeforeNewStops = null;
for (int i = position - 1; i >= 0; i--) {
if (movedSet.contains(currentRoute[i].getId()) == false) {
stopBeforeNewStops = currentRoute[i];
break;
}
}
Task stopAfterNewStops = null;
for (int i = position; i < currentRoute.length; i++) {
if (movedSet.contains(currentRoute[i].getId()) == false) {
stopAfterNewStops = currentRoute[i];
break;
}
}
// now parse and update the raw stop-order table
if (ioDs.isInTransaction()) {
showMessage("Cannot move stops as datastore has an open transaction.");
return false;
}
ioDs.startTransaction();
BeanTableMappingImpl beanMapping = new ScheduleEditorComponent().getBeanMapping().getTableMapping(ScheduleEditorConstants.TASK_ORDER_TABLE_INDEX);
try {
// remove all stops being moved from the stop-order table
int row = 0;
while (row < stopsOrder.getRowCount()) {
// should this stop be removed?
TaskOrder record = (TaskOrder) beanMapping.readObjectFromTableByRow(stopsOrder, row);
if (movedSet.contains(record.getTaskId())) {
stopsOrder.deleteRow(row);
} else {
row++;
}
}
// also remove from our internal record of not loads order
Iterator<Task> it = notLoadsOrder.iterator();
while(it.hasNext()){
if(movedSet.contains(it.next().getId())){
it.remove();
}
}
// add new ones to the stops order table if we're not 'adding' to the unassigned vehicle
if (Strings.equalsStd(vehicleId, ScheduleEditorConstants.UNLOADED_VEHICLE) == false) {
boolean addedNewStops = false;
int n = stopsOrder.getRowCount();
for (row = 0; row < n; row++) {
// is this a stop we should add the inserted stops before?
TaskOrder record = (TaskOrder) beanMapping.readObjectFromTableByRow(stopsOrder, row);
if (stopAfterNewStops != null && Strings.equalsStd(record.getTaskId(), stopAfterNewStops.getId())) {
addTasks(vehicleId, stopIds, beanMapping, stopsOrder, row);
addedNewStops = true;
break;
}
// is this a stop we should add the inserted stops after?
if (stopBeforeNewStops != null && Strings.equalsStd(record.getTaskId(), stopBeforeNewStops.getId())) {
addTasks(vehicleId, stopIds, beanMapping, stopsOrder, row + 1);
addedNewStops = true;
break;
}
}
// add at the end if all else fails
if (!addedNewStops) {
addTasks(vehicleId, stopIds, beanMapping, stopsOrder, stopsOrder.getRowCount());
}
}
else{
// update the internal not-loads array
for(int i = stopIds.length-1;i>=0;i--){
Task stop = data.getTask(stopIds[i]);
if(stop==null){
throw new RuntimeException("Unknown stop-id " + stopIds[i]);
}
notLoadsOrder.add(Math.min(position,notLoadsOrder.size()), stop);
}
}
// save the changes
ioDs.endTransaction();
// Update all controls. This call is probably not needed when running this class
// from a component as the framework will call an update anyway...
setData(api,ioDs);
} catch (Exception e) {
ioDs.rollbackTransaction();
e.printStackTrace();
showMessage("An error occurred when moving stops.");
return false;
}
// set the new data
return true;
}
private void showMessage(String message) {
JOptionPane.showMessageDialog(this, message);
}
private void addTasks(String vehicleId, String[] stopIdsToAdd, BeanTableMappingImpl beanMapping, ODLTable table, int row) {
for (String id : stopIdsToAdd) {
if (data.getTask(id) == null) {
throw new RuntimeException("Unknown stop-id " + id);
}
TaskOrder so = new TaskOrder();
so.setResourceId(vehicleId);
so.setTaskId(id);
table.insertEmptyRow(row, -1);
long rowId = table.getRowId(row);
row++;
beanMapping.updateTableRow(so, table, rowId);
}
}
@Override
public void dispose() {
closeSingleScheduleWindows();
}
// /**
// *
// */
// private void tileRoutes() {
// for(JInternalFrame frame:desktopPane.getAllFrames()){
// try {
// frame.setMaximum(false);
// } catch (PropertyVetoException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
// TileInternalFrames.tile(desktopPane);
// }
/**
*
*/
private void closeSingleScheduleWindows() {
for(JPanel frame : api.getRegisteredPanels()){
if(SingleScheduleFrame.class.isInstance(frame)){
api.disposeRegisteredPanel(frame);
}
}
}
@Override
public EditorData getData(){
return data;
}
}