Java tutorial
/* * Copyright (c) 2009 INSciTE. All rights reserved * INSciTE is on the web at: http://www.hightechkids.org * This code is released under GPL; see LICENSE.txt for details. */ package fll.scheduler; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Component; import java.awt.Container; import java.awt.GridLayout; import java.awt.event.ActionEvent; import java.awt.event.ComponentAdapter; import java.awt.event.ComponentEvent; import java.awt.event.KeyEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.net.URL; import java.sql.SQLException; import java.text.ParseException; import java.time.LocalTime; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Formatter; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.concurrent.ExecutionException; import java.util.prefs.Preferences; import java.util.stream.Collectors; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Box; import javax.swing.JCheckBox; import javax.swing.JFileChooser; import javax.swing.JFormattedTextField; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JMenu; import javax.swing.JMenuBar; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTable; import javax.swing.JToolBar; import javax.swing.KeyStroke; import javax.swing.SwingConstants; import javax.swing.SwingWorker; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.WindowConstants; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.filechooser.FileFilter; import javax.swing.table.DefaultTableCellRenderer; import javax.swing.table.TableCellRenderer; import org.apache.commons.io.IOUtils; import org.apache.log4j.Logger; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.w3c.dom.Document; import com.itextpdf.text.DocumentException; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import fll.Utilities; import fll.scheduler.SchedParams.InvalidParametersException; import fll.scheduler.TournamentSchedule.ColumnInformation; import fll.util.CSVCellReader; import fll.util.CellFileReader; import fll.util.ExcelCellReader; import fll.util.FLLInternalException; import fll.util.FLLRuntimeException; import fll.util.GuiExceptionHandler; import fll.util.LogUtils; import fll.util.ProgressDialog; import fll.xml.ChallengeDescription; import fll.xml.ChallengeParser; import fll.xml.ScoreCategory; import net.mtu.eggplant.util.BasicFileFilter; import net.mtu.eggplant.util.gui.GraphicsUtils; /** * UI for the scheduler. */ public class SchedulerUI extends JFrame { public static void main(final String[] args) { LogUtils.initializeLogging(); Thread.setDefaultUncaughtExceptionHandler(new GuiExceptionHandler()); // Use cross platform look and feel so that things look right all of the // time try { UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName()); } catch (final ClassNotFoundException e) { LOGGER.warn("Could not find cross platform look and feel class", e); } catch (final InstantiationException e) { LOGGER.warn("Could not instantiate cross platform look and feel class", e); } catch (final IllegalAccessException e) { LOGGER.warn("Error loading cross platform look and feel", e); } catch (final UnsupportedLookAndFeelException e) { LOGGER.warn("Cross platform look and feel unsupported?", e); } try { final SchedulerUI frame = new SchedulerUI(); frame.addWindowListener(new WindowAdapter() { @Override @SuppressFBWarnings(value = { "DM_EXIT" }, justification = "Exiting from main is OK") public void windowClosing(final WindowEvent e) { System.exit(0); } @Override @SuppressFBWarnings(value = { "DM_EXIT" }, justification = "Exiting from main is OK") public void windowClosed(final WindowEvent e) { System.exit(0); } }); // should be able to watch for window closing, but hidden works frame.addComponentListener(new ComponentAdapter() { @Override @SuppressFBWarnings(value = { "DM_EXIT" }, justification = "Exiting from main is OK") public void componentHidden(final ComponentEvent e) { System.exit(0); } }); GraphicsUtils.centerWindow(frame); frame.setVisible(true); } catch (final Exception e) { LOGGER.fatal("Unexpected error", e); JOptionPane.showMessageDialog(null, "An unexpected error occurred. Please send the log file and a description of what you were doing to the developer. Error message: " + e.getMessage(), "Error", JOptionPane.ERROR_MESSAGE); } } private final ChooseChallengeDescriptor chooseChallengeDescriptor = new ChooseChallengeDescriptor( SchedulerUI.this); private static final String BASE_TITLE = "FLL Scheduler"; public SchedulerUI() { super(BASE_TITLE); setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); _progressDialog = new ProgressDialog(SchedulerUI.this, "Please Wait"); _progressDialog.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); setJMenuBar(createMenubar()); final Container cpane = getContentPane(); cpane.setLayout(new BorderLayout()); mTabbedPane = new JTabbedPane(); cpane.add(mTabbedPane, BorderLayout.CENTER); final JPanel scheduleDescriptionPanel = new JPanel(new BorderLayout()); mTabbedPane.addTab("Description", scheduleDescriptionPanel); mDescriptionFilename = new JLabel(""); scheduleDescriptionPanel.add(createDescriptionToolbar(), BorderLayout.PAGE_START); mScheduleDescriptionEditor = new SolverParamsEditor(); final JScrollPane editorScroller = new JScrollPane(mScheduleDescriptionEditor); scheduleDescriptionPanel.add(editorScroller, BorderLayout.CENTER); // start out with default values mScheduleDescriptionEditor.setParams(new SolverParams()); final JPanel schedulePanel = new JPanel(new BorderLayout()); mTabbedPane.addTab("Schedule", schedulePanel); mScheduleFilename = new JLabel(""); schedulePanel.add(createScheduleToolbar(), BorderLayout.PAGE_START); mScheduleTable = new JTable(); mScheduleTable.setAutoCreateRowSorter(true); mScheduleTable.setDefaultRenderer(Date.class, schedTableRenderer); mScheduleTable.setDefaultRenderer(String.class, schedTableRenderer); mScheduleTable.setDefaultRenderer(Integer.class, schedTableRenderer); mScheduleTable.setDefaultRenderer(Object.class, schedTableRenderer); final JScrollPane dataScroller = new JScrollPane(mScheduleTable); violationTable = new JTable(); violationTable.setDefaultRenderer(String.class, violationTableRenderer); violationTable.getSelectionModel().addListSelectionListener(violationSelectionListener); final JScrollPane violationScroller = new JScrollPane(violationTable); final JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, dataScroller, violationScroller); schedulePanel.add(splitPane, BorderLayout.CENTER); // initial state mWriteSchedulesAction.setEnabled(false); mDisplayGeneralScheduleAction.setEnabled(false); mRunOptimizerAction.setEnabled(false); mReloadFileAction.setEnabled(false); pack(); } @SuppressFBWarnings(value = "SE_TRANSIENT_FIELD_NOT_RESTORED", justification = "There is no state needed to be kept here") private final transient ListSelectionListener violationSelectionListener = new ListSelectionListener() { public void valueChanged(final ListSelectionEvent e) { final int selectedRow = getViolationTable().getSelectedRow(); if (selectedRow == -1) { return; } final ConstraintViolation selected = getViolationsModel().getViolation(selectedRow); if (ConstraintViolation.NO_TEAM != selected.getTeam()) { final int teamIndex = getScheduleModel().getIndexOfTeam(selected.getTeam()); final int displayIndex = getScheduleTable().convertRowIndexToView(teamIndex); getScheduleTable().changeSelection(displayIndex, 1, false, false); } } }; void saveScheduleDescription() { if (null == mScheduleDescriptionFile) { // prompt the user for a filename to save to final String startingDirectory = PREFS.get(DESCRIPTION_STARTING_DIRECTORY_PREF, null); final JFileChooser fileChooser = new JFileChooser(); final FileFilter filter = new BasicFileFilter("FLL Schedule Description (properties)", new String[] { "properties" }); fileChooser.setFileFilter(filter); if (null != startingDirectory) { fileChooser.setCurrentDirectory(new File(startingDirectory)); } final int returnVal = fileChooser.showSaveDialog(SchedulerUI.this); if (returnVal == JFileChooser.APPROVE_OPTION) { final File currentDirectory = fileChooser.getCurrentDirectory(); PREFS.put(DESCRIPTION_STARTING_DIRECTORY_PREF, currentDirectory.getAbsolutePath()); mScheduleDescriptionFile = fileChooser.getSelectedFile(); mDescriptionFilename.setText(mScheduleDescriptionFile.getName()); } else { // user canceled return; } } try (final Writer writer = new OutputStreamWriter(new FileOutputStream(mScheduleDescriptionFile), Utilities.DEFAULT_CHARSET)) { final SolverParams params = mScheduleDescriptionEditor.getParams(); final List<String> errors = params.isValid(); if (!errors.isEmpty()) { final String formattedErrors = errors.stream().collect(Collectors.joining("\n")); JOptionPane.showMessageDialog(SchedulerUI.this, "There are errors that need to be corrected before the description can be saved: " + formattedErrors, "Error saving file", JOptionPane.ERROR_MESSAGE); } else { final Properties properties = new Properties(); params.save(properties); properties.store(writer, null); } } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error saving file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error saving file", JOptionPane.ERROR_MESSAGE); } } private final Action mSaveScheduleDescriptionAction = new AbstractAction("Save Schedule Description") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Save16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Save24.gif")); putValue(SHORT_DESCRIPTION, "Save the schedule description file"); putValue(MNEMONIC_KEY, KeyEvent.VK_S); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK)); } @Override public void actionPerformed(final ActionEvent ae) { saveScheduleDescription(); } }; private final Action mNewScheduleDescriptionAction = new AbstractAction("New Schedule Description") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/New16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/New24.gif")); putValue(SHORT_DESCRIPTION, "Createa a new schedule description file"); putValue(MNEMONIC_KEY, KeyEvent.VK_N); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK)); } @Override public void actionPerformed(final ActionEvent ae) { newScheduleDescription(); } }; private void newScheduleDescription() { final int result = JOptionPane.showConfirmDialog(SchedulerUI.this, "This action will remove any changes to the current schedule and load the defaults. Do you want to continue?", "Question", JOptionPane.YES_NO_OPTION); if (JOptionPane.YES_OPTION == result) { mScheduleDescriptionFile = null; mDescriptionFilename.setText(""); final SolverParams params = new SolverParams(); mScheduleDescriptionEditor.setParams(params); } } /** * Run the scheduler and optionally the table optimizer. */ private void runScheduler() { try { saveScheduleDescription(); final SchedulerWorker worker = new SchedulerWorker(); // make sure the task doesn't start until the window is up _progressDialog.addComponentListener(new ComponentAdapter() { @Override public void componentShown(final ComponentEvent e) { _progressDialog.removeComponentListener(this); worker.execute(); } }); _progressDialog.setLocationRelativeTo(SchedulerUI.this); _progressDialog.setNote("Running Scheduler"); _progressDialog.setVisible(true); } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reading description file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error Running Scheduler", JOptionPane.ERROR_MESSAGE); } catch (final ParseException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error parsing description file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error Running Scheduler", JOptionPane.ERROR_MESSAGE); } catch (final InvalidParametersException e) { LOGGER.error(e.getMessage(), e); JOptionPane.showMessageDialog(SchedulerUI.this, e.getMessage(), "Error Running Scheduler", JOptionPane.ERROR_MESSAGE); } } private final class SchedulerWorker extends SwingWorker<Integer, Void> { private final GreedySolver solver; public SchedulerWorker() throws IOException, ParseException, InvalidParametersException { this.solver = new GreedySolver(mScheduleDescriptionFile, false); } @Override protected Integer doInBackground() { return solver.solve(_progressDialog); } @Override protected void done() { _progressDialog.setVisible(false); try { final int numSolutions = this.get(); if (numSolutions < 1) { if (_progressDialog.isCanceled()) { JOptionPane.showMessageDialog(SchedulerUI.this, "Scheduler was canceled"); return; } JOptionPane.showMessageDialog(SchedulerUI.this, "No solution found"); return; } final List<SubjectiveStation> subjectiveStations = solver.getParameters().getSubjectiveStations(); // this causes mSchedParams, mScheduleData and mScheduleFile to be set final File solutionFile = solver.getBestSchedule(); if (null == solutionFile) { JOptionPane.showMessageDialog(SchedulerUI.this, "No valid schedule found", "Error Running Scheduler", JOptionPane.ERROR_MESSAGE); return; } loadScheduleFile(solutionFile, subjectiveStations); final int result = JOptionPane.showConfirmDialog(SchedulerUI.this, "Would you like to run the table optimizer?", "Question", JOptionPane.YES_NO_OPTION); if (JOptionPane.YES_OPTION == result) { runTableOptimizer(); } } catch (final ExecutionException e) { LOGGER.error("Error executing scheduler", e); JOptionPane.showMessageDialog(SchedulerUI.this, e.getMessage(), "Error running scheduler", JOptionPane.ERROR_MESSAGE); } catch (final InterruptedException e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Received interrupted exception running scheduler"); } JOptionPane.showMessageDialog(SchedulerUI.this, "Scheduler was interrupted before completing"); return; } } } private final ProgressDialog _progressDialog; private final class OptimizerWorker extends SwingWorker<Void, Void> { private final TableOptimizer optimizer; public OptimizerWorker() { this.optimizer = new TableOptimizer(mSchedParams, mScheduleData, mScheduleFile.getAbsoluteFile().getParentFile()); } @Override protected Void doInBackground() { // see if we can get a better solution optimizer.optimize(_progressDialog); return null; } @Override protected void done() { _progressDialog.setVisible(false); try { this.get(); final File optimizedFile = optimizer.getBestScheduleOutputFile(); if (null != optimizedFile) { loadScheduleFile(optimizedFile, mSchedParams.getSubjectiveStations()); } else { JOptionPane.showMessageDialog(SchedulerUI.this, "No better schedule found"); } } catch (final ExecutionException e) { LOGGER.error("Error executing table optimizer", e); JOptionPane.showMessageDialog(SchedulerUI.this, e.getMessage(), "Error running table optimizer", JOptionPane.ERROR_MESSAGE); } catch (final InterruptedException e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Received interrupted exception running table optimizer"); } return; } } } private final Action mRunSchedulerAction = new AbstractAction("Run Scheduler") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/TipOfTheDay16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/TipOfTheDay24.gif")); putValue(SHORT_DESCRIPTION, "Run the scheduler on the current description"); // putValue(MNEMONIC_KEY, KeyEvent.VK_S); } @Override public void actionPerformed(final ActionEvent ae) { runScheduler(); } }; private final Action mOpenScheduleDescriptionAction = new AbstractAction("Open Schedule Description") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Open16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Open24.gif")); putValue(SHORT_DESCRIPTION, "Open the schedule description file"); // putValue(MNEMONIC_KEY, KeyEvent.VK_S); } @Override public void actionPerformed(final ActionEvent ae) { final String startingDirectory = PREFS.get(DESCRIPTION_STARTING_DIRECTORY_PREF, null); final JFileChooser fileChooser = new JFileChooser(); final FileFilter filter = new BasicFileFilter("FLL Schedule Description (properties)", new String[] { "properties" }); fileChooser.setFileFilter(filter); if (null != startingDirectory) { fileChooser.setCurrentDirectory(new File(startingDirectory)); } final int returnVal = fileChooser.showOpenDialog(SchedulerUI.this); if (returnVal == JFileChooser.APPROVE_OPTION) { final File currentDirectory = fileChooser.getCurrentDirectory(); PREFS.put(DESCRIPTION_STARTING_DIRECTORY_PREF, currentDirectory.getAbsolutePath()); final File selectedFile = fileChooser.getSelectedFile(); if (null != selectedFile && selectedFile.isFile() && selectedFile.canRead()) { loadScheduleDescription(selectedFile); } else if (null != selectedFile) { JOptionPane.showMessageDialog(SchedulerUI.this, new Formatter().format("%s is not a file or is not readable", selectedFile.getAbsolutePath()), "Error reading file", JOptionPane.ERROR_MESSAGE); } } } }; private void loadScheduleDescription(final File file) { final Properties properties = new Properties(); try (final Reader reader = new InputStreamReader(new FileInputStream(file), Utilities.DEFAULT_CHARSET)) { properties.load(reader); } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error loading file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error loading file", JOptionPane.ERROR_MESSAGE); return; } if (LOGGER.isDebugEnabled()) { LOGGER.debug(properties.toString()); } try { final SolverParams params = new SolverParams(); params.load(properties); mScheduleDescriptionEditor.setParams(params); } catch (final ParseException pe) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error parsing file: %s", pe.getMessage()); LOGGER.error(errorFormatter, pe); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error parsing file", JOptionPane.ERROR_MESSAGE); return; } mScheduleDescriptionFile = file; mDescriptionFilename.setText(file.getName()); } private JToolBar createDescriptionToolbar() { final JToolBar toolbar = new JToolBar("Description Toolbar"); toolbar.setFloatable(false); toolbar.add(mDescriptionFilename); toolbar.addSeparator(); toolbar.add(mNewScheduleDescriptionAction); toolbar.add(mOpenScheduleDescriptionAction); toolbar.add(mSaveScheduleDescriptionAction); toolbar.add(mRunSchedulerAction); return toolbar; } private JToolBar createScheduleToolbar() { final JToolBar toolbar = new JToolBar("Schedule Toolbar"); toolbar.setFloatable(false); toolbar.add(mScheduleFilename); toolbar.addSeparator(); toolbar.add(mOpenScheduleAction); toolbar.add(mReloadFileAction); toolbar.add(mWriteSchedulesAction); toolbar.add(mDisplayGeneralScheduleAction); return toolbar; } private JMenuBar createMenubar() { final JMenuBar menubar = new JMenuBar(); menubar.add(createFileMenu()); menubar.add(createDescriptionMenu()); menubar.add(createScheduleMenu()); return menubar; } private JMenu createScheduleMenu() { final JMenu menu = new JMenu("Schedule"); menu.setMnemonic('s'); menu.add(mOpenScheduleAction); menu.add(mReloadFileAction); menu.add(mRunOptimizerAction); menu.add(mWriteSchedulesAction); menu.add(mDisplayGeneralScheduleAction); return menu; } private JMenu createDescriptionMenu() { final JMenu menu = new JMenu("Description"); menu.setMnemonic('d'); menu.add(mNewScheduleDescriptionAction); menu.add(mOpenScheduleDescriptionAction); menu.add(mSaveScheduleDescriptionAction); menu.add(mRunSchedulerAction); return menu; } private JMenu createFileMenu() { final JMenu menu = new JMenu("File"); menu.setMnemonic('f'); menu.add(mPreferencesAction); menu.add(mExitAction); return menu; } private final Action mReloadFileAction = new AbstractAction("Reload Schedule") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Refresh16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Refresh24.gif")); putValue(SHORT_DESCRIPTION, "Reload the file"); // putValue(MNEMONIC_KEY, KeyEvent.VK_X); } @Override public void actionPerformed(final ActionEvent ae) { FileInputStream fis = null; try { final File selectedFile = getScheduleFile(); final String sheetName = getCurrentSheetName(); final String name = Utilities.extractBasename(selectedFile); final TournamentSchedule newData; if (null == sheetName) { // if no sheet name, assume CSV file newData = new TournamentSchedule(name, selectedFile, mScheduleData.getSubjectiveStations()); } else { fis = new FileInputStream(selectedFile); newData = new TournamentSchedule(name, fis, sheetName, mScheduleData.getSubjectiveStations()); } setScheduleData(newData); } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reloading file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reloading file", JOptionPane.ERROR_MESSAGE); } catch (ParseException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reloading file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reloading file", JOptionPane.ERROR_MESSAGE); } catch (final InvalidFormatException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reloading file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reloading file", JOptionPane.ERROR_MESSAGE); } catch (final ScheduleParseException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error parsing file: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error parsing file", JOptionPane.ERROR_MESSAGE); } finally { try { if (null != fis) { fis.close(); } } catch (final IOException e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Exception closing stream", e); } } } } }; private final Action mPreferencesAction = new AbstractAction("Preferences") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Preferences16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Preferences24.gif")); putValue(SHORT_DESCRIPTION, "Set scheduling preferences"); // putValue(MNEMONIC_KEY, KeyEvent.VK_X); } @Override public void actionPerformed(final ActionEvent ae) { JOptionPane.showMessageDialog(SchedulerUI.this, "Not implemented yet"); } }; private final Action mExitAction = new AbstractAction("Exit") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Stop16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Stop24.gif")); putValue(SHORT_DESCRIPTION, "Exit the application"); putValue(MNEMONIC_KEY, KeyEvent.VK_X); putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK)); } public void actionPerformed(final ActionEvent ae) { SchedulerUI.this.setVisible(false); } }; private final Action mDisplayGeneralScheduleAction = new AbstractAction("General Schedule") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/History16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/History24.gif")); putValue(SHORT_DESCRIPTION, "Display the general schedule"); // putValue(MNEMONIC_KEY, KeyEvent.VK_X); } @Override public void actionPerformed(final ActionEvent ae) { final String schedule = getScheduleData().computeGeneralSchedule(); JOptionPane.showMessageDialog(SchedulerUI.this, schedule, "General Schedule", JOptionPane.INFORMATION_MESSAGE); } }; private final Action mWriteSchedulesAction = new AbstractAction("Write Detailed Schedules") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Export16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Export24.gif")); putValue(SHORT_DESCRIPTION, "Write out the detailed schedules as a PDF"); // putValue(MNEMONIC_KEY, KeyEvent.VK_X); } @Override public void actionPerformed(final ActionEvent ae) { FileOutputStream scoresheetFos = null; try { final File directory = getScheduleFile().getParentFile(); final String baseFilename = Utilities.extractBasename(getScheduleFile()); LOGGER.info("Writing detailed schedules to " + directory.getAbsolutePath()); getScheduleData().outputDetailedSchedules(getSchedParams(), getScheduleFile().getParentFile(), baseFilename); JOptionPane.showMessageDialog(SchedulerUI.this, "Detailed schedule written '" + directory.getAbsolutePath() + "'", "Information", JOptionPane.INFORMATION_MESSAGE); final int answer = JOptionPane.showConfirmDialog(SchedulerUI.this, "Would you like to write out the score sheets as well?", "Write Out Scoresheets?", JOptionPane.YES_NO_OPTION); if (JOptionPane.YES_OPTION == answer) { chooseChallengeDescriptor.setLocationRelativeTo(SchedulerUI.this); chooseChallengeDescriptor.setVisible(true); final URL descriptorLocation = chooseChallengeDescriptor.getSelectedDescription(); if (null != descriptorLocation) { final Reader descriptorReader = new InputStreamReader(descriptorLocation.openStream(), Utilities.DEFAULT_CHARSET); final Document document = ChallengeParser.parse(descriptorReader); final ChallengeDescription description = new ChallengeDescription( document.getDocumentElement()); final File scoresheetFile = new File(directory, baseFilename + "-scoresheets.pdf"); scoresheetFos = new FileOutputStream(scoresheetFile); getScheduleData().outputPerformanceSheets(scoresheetFos, description); final MapSubjectiveHeaders mapDialog = new MapSubjectiveHeaders(SchedulerUI.this, description, getScheduleData()); mapDialog.setLocationRelativeTo(SchedulerUI.this); mapDialog.setVisible(true); if (!mapDialog.isCanceled()) { final Map<ScoreCategory, String> categoryToSchedule = new HashMap<>(); for (final ScoreCategory scoreCategory : description.getSubjectiveCategories()) { final String scheduleColumn = mapDialog .getSubjectiveHeaderForCategory(scoreCategory); if (null == scheduleColumn) { throw new FLLInternalException( "Did not find a schedule column for " + scoreCategory.getTitle()); } categoryToSchedule.put(scoreCategory, scheduleColumn); } getScheduleData().outputSubjectiveSheets(directory.getAbsolutePath(), baseFilename, description, categoryToSchedule); JOptionPane.showMessageDialog(SchedulerUI.this, "Scoresheets written '" + scoresheetFile.getAbsolutePath() + "'", "Information", JOptionPane.INFORMATION_MESSAGE); } // not canceled } // valid descriptor location } // yes to write score sheets } catch (final DocumentException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error writing detailed schedules: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error", JOptionPane.ERROR_MESSAGE); return; } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error writing detailed schedules: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error", JOptionPane.ERROR_MESSAGE); return; } catch (final SQLException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Unexpected Error writing detailed schedules: %s", e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error", JOptionPane.ERROR_MESSAGE); return; } finally { IOUtils.closeQuietly(scoresheetFos); } } }; /** * Run the table optimizer on the current schedule and open the resulting * file. */ private void runTableOptimizer() { final OptimizerWorker optimizerWorker = new OptimizerWorker(); // make sure the task doesn't start until the window is up _progressDialog.addComponentListener(new ComponentAdapter() { @Override public void componentShown(final ComponentEvent e) { _progressDialog.removeComponentListener(this); optimizerWorker.execute(); } }); _progressDialog.setLocationRelativeTo(SchedulerUI.this); _progressDialog.setNote("Running Table Optimizer"); _progressDialog.setVisible(true); } /** * Load the specified schedule file and select the schedule tab. * * @param selectedFile * @param subjectiveStations if not null, use as the subjective stations, * otherwise prompt the user for the subjective stations */ private void loadScheduleFile(final File selectedFile, final List<SubjectiveStation> subjectiveStations) { FileInputStream fis = null; try { final boolean csv = selectedFile.getName().endsWith("csv"); final CellFileReader reader; final String sheetName; if (csv) { reader = new CSVCellReader(selectedFile); sheetName = null; } else { sheetName = promptForSheetName(selectedFile); if (null == sheetName) { return; } fis = new FileInputStream(selectedFile); reader = new ExcelCellReader(fis, sheetName); } final List<SubjectiveStation> newSubjectiveStations; if (null == subjectiveStations) { final ColumnInformation columnInfo = TournamentSchedule.findColumns(reader, new LinkedList<String>()); newSubjectiveStations = gatherSubjectiveStationInformation(SchedulerUI.this, columnInfo); } else { newSubjectiveStations = subjectiveStations; } if (null != fis) { fis.close(); fis = null; } mSchedParams = new SchedParams(newSubjectiveStations, SchedParams.DEFAULT_PERFORMANCE_MINUTES, SchedParams.MINIMUM_CHANGETIME_MINUTES, SchedParams.MINIMUM_PERFORMANCE_CHANGETIME_MINUTES); final List<String> subjectiveHeaders = new LinkedList<String>(); for (final SubjectiveStation station : newSubjectiveStations) { subjectiveHeaders.add(station.getName()); } final String name = Utilities.extractBasename(selectedFile); final TournamentSchedule schedule; if (csv) { schedule = new TournamentSchedule(name, selectedFile, subjectiveHeaders); } else { fis = new FileInputStream(selectedFile); schedule = new TournamentSchedule(name, fis, sheetName, subjectiveHeaders); } mScheduleFile = selectedFile; mScheduleSheetName = sheetName; setScheduleData(schedule); setTitle(BASE_TITLE + " - " + mScheduleFile.getName() + ":" + mScheduleSheetName); mWriteSchedulesAction.setEnabled(true); mDisplayGeneralScheduleAction.setEnabled(true); mRunOptimizerAction.setEnabled(true); mReloadFileAction.setEnabled(true); mScheduleFilename.setText(mScheduleFile.getName()); mTabbedPane.setSelectedIndex(1); } catch (final ParseException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reading file %s: %s", selectedFile.getAbsolutePath(), e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reading file", JOptionPane.ERROR_MESSAGE); return; } catch (final IOException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error reading file %s: %s", selectedFile.getAbsolutePath(), e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reading file", JOptionPane.ERROR_MESSAGE); return; } catch (final InvalidFormatException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Unknown file format %s: %s", selectedFile.getAbsolutePath(), e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error reading file", JOptionPane.ERROR_MESSAGE); return; } catch (final ScheduleParseException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error parsing file %s: %s", selectedFile.getAbsolutePath(), e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error parsing file", JOptionPane.ERROR_MESSAGE); return; } catch (final FLLRuntimeException e) { final Formatter errorFormatter = new Formatter(); errorFormatter.format("Error parsing file %s: %s", selectedFile.getAbsolutePath(), e.getMessage()); LOGGER.error(errorFormatter, e); JOptionPane.showMessageDialog(SchedulerUI.this, errorFormatter, "Error parsing file", JOptionPane.ERROR_MESSAGE); return; } finally { try { if (null != fis) { fis.close(); } } catch (final IOException e) { if (LOGGER.isDebugEnabled()) { LOGGER.debug("Error closing stream", e); } } } } private final Action mRunOptimizerAction = new AbstractAction("Run Table Optimizer") { @Override public void actionPerformed(final ActionEvent ae) { runTableOptimizer(); } }; private final Action mOpenScheduleAction = new AbstractAction("Open Schedule") { { putValue(SMALL_ICON, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Open16.gif")); putValue(LARGE_ICON_KEY, GraphicsUtils.getIcon("toolbarButtonGraphics/general/Open24.gif")); putValue(SHORT_DESCRIPTION, "Open a schedule file"); putValue(MNEMONIC_KEY, KeyEvent.VK_O); } @Override public void actionPerformed(final ActionEvent ae) { final String startingDirectory = PREFS.get(SCHEDULE_STARTING_DIRECTORY_PREF, null); final JFileChooser fileChooser = new JFileChooser(); final FileFilter filter = new BasicFileFilter("FLL Schedule (xls, xlsx, csv)", new String[] { "xls", "xlsx", "csv" }); fileChooser.setFileFilter(filter); if (null != startingDirectory) { fileChooser.setCurrentDirectory(new File(startingDirectory)); } final int returnVal = fileChooser.showOpenDialog(SchedulerUI.this); if (returnVal == JFileChooser.APPROVE_OPTION) { final File currentDirectory = fileChooser.getCurrentDirectory(); PREFS.put(SCHEDULE_STARTING_DIRECTORY_PREF, currentDirectory.getAbsolutePath()); final File selectedFile = fileChooser.getSelectedFile(); if (null != selectedFile && selectedFile.isFile() && selectedFile.canRead()) { loadScheduleFile(selectedFile, null); } else if (null != selectedFile) { JOptionPane.showMessageDialog(SchedulerUI.this, new Formatter().format("%s is not a file or is not readable", selectedFile.getAbsolutePath()), "Error reading file", JOptionPane.ERROR_MESSAGE); } } } }; /** * If there is more than 1 sheet, prompt, otherwise just use the sheet. * * @return the sheet name or null if the user canceled * @throws IOException * @throws InvalidFormatException */ public static String promptForSheetName(final File selectedFile) throws InvalidFormatException, IOException { final List<String> sheetNames = ExcelCellReader.getAllSheetNames(selectedFile); if (sheetNames.size() == 1) { return sheetNames.get(0); } else { final String[] options = sheetNames.toArray(new String[sheetNames.size()]); final int choosenOption = JOptionPane.showOptionDialog(null, "Choose which sheet to work with", "Choose Sheet", JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[0]); if (JOptionPane.CLOSED_OPTION == choosenOption) { return null; } return options[choosenOption]; } } private static final Preferences PREFS = Preferences.userNodeForPackage(TournamentSchedule.class); private static final String SCHEDULE_STARTING_DIRECTORY_PREF = "scheduleStartingDirectory"; private static final String DESCRIPTION_STARTING_DIRECTORY_PREF = "descriptionStartingDirectory"; @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "This calss isn't going to be serialized") private TournamentSchedule mScheduleData; /* package */TournamentSchedule getScheduleData() { return mScheduleData; } private SchedParams mSchedParams; public SchedParams getSchedParams() { return mSchedParams; } private SchedulerTableModel mScheduleModel; SchedulerTableModel getScheduleModel() { return mScheduleModel; } private ViolationTableModel mViolationsModel; /* package */ViolationTableModel getViolationsModel() { return mViolationsModel; } private void setScheduleData(final TournamentSchedule sd) { mScheduleTable.clearSelection(); mScheduleData = sd; mScheduleModel = new SchedulerTableModel(mScheduleData); mScheduleTable.setModel(mScheduleModel); checkSchedule(); } /** * Verify the existing schedule and update the violations. */ private void checkSchedule() { violationTable.clearSelection(); final ScheduleChecker checker = new ScheduleChecker(getSchedParams(), getScheduleData()); mViolationsModel = new ViolationTableModel(checker.verifySchedule()); violationTable.setModel(mViolationsModel); } private final JLabel mDescriptionFilename; private final JLabel mScheduleFilename; private final JTabbedPane mTabbedPane; private final SolverParamsEditor mScheduleDescriptionEditor; private final JTable mScheduleTable; private JTable getScheduleTable() { return mScheduleTable; } private final JTable violationTable; JTable getViolationTable() { return violationTable; } private static final Logger LOGGER = LogUtils.getLogger(); private static final Color HARD_CONSTRAINT_COLOR = Color.RED; private static final Color SOFT_CONSTRAINT_COLOR = Color.YELLOW; private TableCellRenderer schedTableRenderer = new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { setHorizontalAlignment(CENTER); final int tmRow = table.convertRowIndexToModel(row); final int tmCol = table.convertColumnIndexToModel(column); final TeamScheduleInfo schedInfo = getScheduleModel().getSchedInfo(tmRow); if (LOGGER.isTraceEnabled()) { LOGGER.trace("Checking for violations against team: " + schedInfo.getTeamNumber() // + " column: " + tmCol // + " row: " + tmRow // ); } boolean error = false; boolean isHard = false; for (final ConstraintViolation violation : getViolationsModel().getViolations()) { if (violation.getTeam() == schedInfo.getTeamNumber()) { Collection<SubjectiveTime> subjectiveTimes = violation.getSubjectiveTimes(); if (tmCol <= SchedulerTableModel.JUDGE_COLUMN && subjectiveTimes.isEmpty() && null == violation.getPerformance()) { // there is an error for this team and the team information fields // should be highlighted error = true; isHard |= violation.isHard(); } else if (null != violation.getPerformance()) { // need to check round which round int round = 0; // using Math.min to handle extra round while (!violation.getPerformance() .equals(schedInfo.getPerfTime(Math.min(schedInfo.getNumberOfRounds() - 1, round))) && round < schedInfo.getNumberOfRounds()) { ++round; if (round > schedInfo.getNumberOfRounds()) { throw new RuntimeException("Internal error, walked off the end of the round list"); } } // handle extra run if (round >= schedInfo.getNumberOfRounds()) { round = schedInfo.getNumberOfRounds() - 1; } final int firstIdx = getScheduleModel().getFirstPerformanceColumn() + (round * SchedulerTableModel.NUM_COLUMNS_PER_ROUND); final int lastIdx = firstIdx + SchedulerTableModel.NUM_COLUMNS_PER_ROUND - 1; if (firstIdx <= tmCol && tmCol <= lastIdx) { error = true; isHard |= violation.isHard(); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Violation is in performance round: " + round // + " team: " + schedInfo.getTeamNumber() // + " firstIdx: " + firstIdx // + " lastIdx: " + lastIdx // + " column: " + tmCol // + " error: " + error // ); } } else { for (final SubjectiveTime subj : subjectiveTimes) { if (tmCol == getScheduleModel().getColumnForSubjective(subj.getName())) { error = true; isHard |= violation.isHard(); } } } } } // set the background based on the error state setForeground(null); setBackground(null); if (error) { final Color violationColor = isHard ? HARD_CONSTRAINT_COLOR : SOFT_CONSTRAINT_COLOR; if (isSelected) { setForeground(violationColor); } else { setBackground(violationColor); } } if (value instanceof LocalTime) { final String strValue = TournamentSchedule.formatTime((LocalTime) value); return super.getTableCellRendererComponent(table, strValue, isSelected, hasFocus, row, column); } else { return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } } }; private TableCellRenderer violationTableRenderer = new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column) { setHorizontalAlignment(CENTER); final int tmRow = table.convertRowIndexToModel(row); final ConstraintViolation violation = getViolationsModel().getViolation(tmRow); final Color violationColor = violation.isHard() ? HARD_CONSTRAINT_COLOR : SOFT_CONSTRAINT_COLOR; setForeground(null); setBackground(null); if (isSelected) { setForeground(violationColor); } else { setBackground(violationColor); } setHorizontalAlignment(SwingConstants.LEFT); return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); } }; private File mScheduleDescriptionFile = null; private File mScheduleFile; protected File getScheduleFile() { return mScheduleFile; } private String mScheduleSheetName; protected String getCurrentSheetName() { return mScheduleSheetName; } /** * Prompt the user for which columns represent subjective categories. * * @param parentComponent the parent for the dialog * @param columnInfo the column information * @return the list of subjective information the user choose */ public static List<SubjectiveStation> gatherSubjectiveStationInformation(final Component parentComponent, final ColumnInformation columnInfo) { final List<String> unusedColumns = columnInfo.getUnusedColumns(); final List<JCheckBox> checkboxes = new LinkedList<JCheckBox>(); final List<JFormattedTextField> subjectiveDurations = new LinkedList<JFormattedTextField>(); final Box optionPanel = Box.createVerticalBox(); optionPanel.add(new JLabel("Specify which columns in the data file are for subjective judging")); final JPanel grid = new JPanel(new GridLayout(0, 2)); optionPanel.add(grid); grid.add(new JLabel("Data file column")); grid.add(new JLabel("Duration (minutes)")); for (final String column : unusedColumns) { if (null != column && column.length() > 0) { final JCheckBox checkbox = new JCheckBox(column); checkboxes.add(checkbox); final JFormattedTextField duration = new JFormattedTextField( Integer.valueOf(SchedParams.DEFAULT_SUBJECTIVE_MINUTES)); duration.setColumns(4); subjectiveDurations.add(duration); grid.add(checkbox); grid.add(duration); } } final List<SubjectiveStation> subjectiveHeaders; if (!checkboxes.isEmpty()) { JOptionPane.showMessageDialog(parentComponent, optionPanel, "Choose Subjective Columns", JOptionPane.QUESTION_MESSAGE); subjectiveHeaders = new LinkedList<SubjectiveStation>(); for (int i = 0; i < checkboxes.size(); ++i) { final JCheckBox box = checkboxes.get(i); final JFormattedTextField duration = subjectiveDurations.get(i); if (box.isSelected()) { subjectiveHeaders .add(new SubjectiveStation(box.getText(), ((Number) duration.getValue()).intValue())); } } } else { subjectiveHeaders = Collections.emptyList(); } if (LOGGER.isTraceEnabled()) { LOGGER.trace("Subjective headers selected: " + subjectiveHeaders); } return subjectiveHeaders; } }