1 package tim.prune.load;
3 import java.awt.BorderLayout;
4 import java.awt.CardLayout;
5 import java.awt.Component;
6 import java.awt.Dimension;
7 import java.awt.FlowLayout;
8 import java.awt.GridLayout;
9 import java.awt.event.ActionEvent;
10 import java.awt.event.ActionListener;
12 import javax.swing.event.DocumentEvent;
13 import javax.swing.event.DocumentListener;
14 import javax.swing.event.ListSelectionEvent;
15 import javax.swing.event.ListSelectionListener;
16 import javax.swing.table.TableCellEditor;
21 import tim.prune.I18nManager;
22 import tim.prune.data.Altitude;
23 import tim.prune.data.Field;
27 * Special class to handle file loading including GUI options,
28 * and conversion to a Track object
30 public class FileLoader
32 private File _file = null;
33 private App _app = null;
34 private JFrame _parentFrame = null;
35 private JDialog _dialog = null;
36 private JPanel _cardPanel = null;
37 private CardLayout _layout = null;
38 private JButton _backButton = null, _nextButton = null;
39 private JButton _finishButton = null;
40 private JButton _moveUpButton = null, _moveDownButton = null;
41 private JRadioButton[] _delimiterRadios = null;
42 private JTextField _otherDelimiterText = null;
43 private JLabel _statusLabel = null;
44 private DelimiterInfo[] _delimiterInfos = null;
45 private JFileChooser _fileChooser = null;
46 private FileCacher _fileCacher = null;
47 private JList _snippetBox = null;
48 private FileExtractTableModel _fileExtractTableModel = null;
49 private JTable _fieldTable;
50 private FieldSelectionTableModel _fieldTableModel = null;
51 private JComboBox _unitsDropDown = null;
52 private int _selectedField = -1;
53 private char _currentDelimiter = ',';
55 // previously selected values
56 private char _lastUsedDelimiter = ',';
57 private int _lastNumFields = -1;
58 private Field[] _lastSelectedFields = null;
59 private int _lastAltitudeFormat = Altitude.FORMAT_NONE;
62 private static final int SNIPPET_SIZE = 6;
63 private static final int MAX_SNIPPET_WIDTH = 80;
64 private static final char[] DELIMITERS = {',', '\t', ';', ' '};
68 * Inner class to listen for delimiter change operations
70 private class DelimListener implements ActionListener, DocumentListener
72 public void actionPerformed(ActionEvent e)
74 informDelimiterSelected();
76 public void changedUpdate(DocumentEvent e)
78 informDelimiterSelected();
80 public void insertUpdate(DocumentEvent e)
82 informDelimiterSelected();
84 public void removeUpdate(DocumentEvent e)
86 informDelimiterSelected();
93 * @param inApp Application object to inform of track load
94 * @param inParentFrame parent frame to reference for dialogs
96 public FileLoader(App inApp, JFrame inParentFrame)
99 _parentFrame = inParentFrame;
104 * Select an input file and open the GUI frame
105 * to select load options
107 public void openFile()
109 if (_fileChooser == null)
110 _fileChooser = new JFileChooser();
111 if (_fileChooser.showOpenDialog(_parentFrame) == JFileChooser.APPROVE_OPTION)
113 _file = _fileChooser.getSelectedFile();
114 if (preCheckFile(_file))
116 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
117 _dialog.setLocationRelativeTo(_parentFrame);
118 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
119 _dialog.getContentPane().add(makeDialogComponents());
121 // select best separator according to row counts (more is better)
122 int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
123 _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
124 _delimiterInfos[3].getNumWinningRecords());
126 _delimiterRadios[bestDelim].setSelected(true);
128 _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
129 informDelimiterSelected();
135 JOptionPane.showMessageDialog(_parentFrame, I18nManager.getText("error.load.noread"),
136 I18nManager.getText("error.load.dialogtitle"), JOptionPane.ERROR_MESSAGE);
143 * Check the given file for readability and funny characters,
144 * and count the fields for the various separators
145 * @param inFile file to check
147 private boolean preCheckFile(File inFile)
149 // Check file exists and is readable
150 if (inFile == null || !inFile.exists() || !inFile.canRead())
154 // Use a FileCacher to read the file into an array
155 _fileCacher = new FileCacher(inFile);
157 // Check each line of the file
158 String[] fileContents = _fileCacher.getContents();
159 boolean fileOK = true;
160 _delimiterInfos = new DelimiterInfo[5];
161 for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
163 String currLine = null;
164 String[] splitFields = null;
165 int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
166 for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
168 currLine = fileContents[lineNum];
169 // check for invalid characters
170 if (currLine.indexOf('\0') >= 0) {fileOK = false;}
172 splitFields = currLine.split(",");
173 commaFields = splitFields.length;
174 if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
175 _delimiterInfos[0].updateMaxFields(commaFields);
177 splitFields = currLine.split("\t");
178 tabFields = splitFields.length;
179 if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
180 _delimiterInfos[1].updateMaxFields(tabFields);
181 // check for semicolons
182 splitFields = currLine.split(";");
183 semicolonFields = splitFields.length;
184 if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
185 _delimiterInfos[2].updateMaxFields(semicolonFields);
187 splitFields = currLine.split(" ");
188 spaceFields = splitFields.length;
189 if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
190 _delimiterInfos[3].updateMaxFields(spaceFields);
191 // increment counters
192 int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
194 _delimiterInfos[bestScorer].incrementNumWinningRecords();
201 * Get the index of the best one in the list
202 * @return the index of the maximum of the four given values
204 private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
208 if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
209 if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
210 if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
211 if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
217 * Make the components for the open options dialog
218 * @return Component for all options
220 private Component makeDialogComponents()
222 JPanel wholePanel = new JPanel();
223 wholePanel.setLayout(new BorderLayout());
225 // add buttons to south
226 JPanel buttonPanel = new JPanel();
227 buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
228 _backButton = new JButton(I18nManager.getText("button.back"));
229 _backButton.addActionListener(new ActionListener() {
230 public void actionPerformed(ActionEvent e)
232 _layout.previous(_cardPanel);
233 _backButton.setEnabled(false);
234 _nextButton.setEnabled(true);
235 _finishButton.setEnabled(false);
238 _backButton.setEnabled(false);
239 buttonPanel.add(_backButton);
240 _nextButton = new JButton(I18nManager.getText("button.next"));
241 _nextButton.addActionListener(new ActionListener() {
242 public void actionPerformed(ActionEvent e)
244 prepareSecondPanel();
245 _layout.next(_cardPanel);
246 _nextButton.setEnabled(false);
247 _backButton.setEnabled(true);
248 _finishButton.setEnabled(true);
251 buttonPanel.add(_nextButton);
252 _finishButton = new JButton(I18nManager.getText("button.finish"));
253 _finishButton.addActionListener(new ActionListener() {
254 public void actionPerformed(ActionEvent e)
259 _finishButton.setEnabled(false);
260 buttonPanel.add(_finishButton);
261 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
262 cancelButton.addActionListener(new ActionListener() {
263 public void actionPerformed(ActionEvent e)
268 buttonPanel.add(cancelButton);
269 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
271 // Make the two cards, for delimiter and fields
272 _cardPanel = new JPanel();
273 _layout = new CardLayout();
274 _cardPanel.setLayout(_layout);
275 JPanel firstCard = new JPanel();
276 firstCard.setLayout(new BorderLayout());
278 JPanel delimsPanel = new JPanel();
279 delimsPanel.setLayout(new GridLayout(0, 2));
280 delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
281 delimsPanel.add(new JLabel("")); // blank label to go to next grid row
283 _delimiterRadios = new JRadioButton[5];
284 _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
285 delimsPanel.add(_delimiterRadios[0]);
286 _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
287 delimsPanel.add(_delimiterRadios[1]);
288 _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
289 delimsPanel.add(_delimiterRadios[2]);
290 _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
291 delimsPanel.add(_delimiterRadios[3]);
292 JPanel otherPanel = new JPanel();
293 otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
294 _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
295 otherPanel.add(_delimiterRadios[4]);
296 _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
297 otherPanel.add(_otherDelimiterText);
298 // Group radio buttons
299 ButtonGroup delimGroup = new ButtonGroup();
300 DelimListener delimListener = new DelimListener();
301 for (int i=0; i<_delimiterRadios.length; i++)
303 delimGroup.add(_delimiterRadios[i]);
304 _delimiterRadios[i].addActionListener(delimListener);
306 _otherDelimiterText.getDocument().addDocumentListener(delimListener);
307 delimsPanel.add(new JLabel(""));
308 delimsPanel.add(otherPanel);
309 _statusLabel = new JLabel("");
310 delimsPanel.add(_statusLabel);
311 firstCard.add(delimsPanel, BorderLayout.SOUTH);
312 // load snippet to show first few lines
313 _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
314 _snippetBox.setEnabled(false);
315 firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
317 // Second screen, for field order selection
318 JPanel secondCard = new JPanel();
319 secondCard.setLayout(new BorderLayout());
320 // table for file contents
321 _fileExtractTableModel = new FileExtractTableModel();
322 JTable extractTable = new JTable(_fileExtractTableModel);
323 JScrollPane tableScrollPane = new JScrollPane(extractTable);
324 extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
325 extractTable.getTableHeader().setReorderingAllowed(false);
326 secondCard.add(makeLabelledPanel("dialog.openoptions.tabledesc", tableScrollPane), BorderLayout.NORTH);
327 JPanel innerPanel2 = new JPanel();
328 innerPanel2.setLayout(new BorderLayout());
329 _fieldTable = new JTable(new FieldSelectionTableModel());
330 _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
331 // add listener for selected table row
332 _fieldTable.getSelectionModel().addListSelectionListener(
333 new ListSelectionListener() {
334 public void valueChanged(ListSelectionEvent e) {
335 ListSelectionModel lsm = (ListSelectionModel) e.getSource();
336 if (lsm.isSelectionEmpty()) {
337 //no rows are selected
340 selectField(lsm.getMinSelectionIndex());
344 JPanel tablePanel = new JPanel();
345 tablePanel.setLayout(new BorderLayout());
346 tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
347 tablePanel.add(_fieldTable, BorderLayout.CENTER);
348 innerPanel2.add(tablePanel, BorderLayout.CENTER);
350 JPanel innerPanel3 = new JPanel();
351 innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
352 _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
353 _moveUpButton.addActionListener(new ActionListener() {
354 public void actionPerformed(ActionEvent e)
356 int currRow = _fieldTable.getSelectedRow();
357 closeTableComboBox(currRow);
358 _fieldTableModel.moveUp(currRow);
359 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
362 innerPanel3.add(_moveUpButton);
363 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
364 _moveDownButton.addActionListener(new ActionListener() {
365 public void actionPerformed(ActionEvent e)
367 int currRow = _fieldTable.getSelectedRow();
368 closeTableComboBox(currRow);
369 _fieldTableModel.moveDown(currRow);
370 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
373 innerPanel3.add(_moveDownButton);
374 innerPanel3.add(Box.createVerticalStrut(70));
376 innerPanel2.add(innerPanel3, BorderLayout.EAST);
377 secondCard.add(innerPanel2, BorderLayout.CENTER);
378 JPanel altUnitsPanel = new JPanel();
379 altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
380 altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
381 String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
382 _unitsDropDown = new JComboBox(units);
383 altUnitsPanel.add(_unitsDropDown);
384 secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
385 _cardPanel.add(firstCard, "card1");
386 _cardPanel.add(secondCard, "card2");
388 wholePanel.add(_cardPanel, BorderLayout.CENTER);
394 * Close the combo box on the selected row of the field table
395 * @param inRow currently selected row number
397 private void closeTableComboBox(int inRow)
399 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
402 editor.stopCellEditing();
408 * change the status based on selection of a delimiter
410 protected void informDelimiterSelected()
412 for (int i=0; i<(_delimiterRadios.length-1); i++)
414 if (_delimiterRadios[i].isSelected())
416 int numRecords = _delimiterInfos[i].getNumRecords();
419 _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
423 _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
424 + _delimiterInfos[i].getMaxFields() + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
428 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
430 _statusLabel.setText("");
432 // enable/disable next button
433 _nextButton.setEnabled(_delimiterRadios[4].isSelected() == false
434 || _otherDelimiterText.getText().length() == 1);
439 * Get the delimiter info from the first step
440 * @return delimiter information object for the selected delimiter
442 public DelimiterInfo getSelectedDelimiterInfo()
444 for (int i=0; i<4; i++)
445 if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
446 // must be "other" - build info if necessary
447 if (_delimiterInfos[4] == null)
448 _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
449 return _delimiterInfos[4];
454 * Use the delimiter selected to determine the fields in the file
455 * and prepare the second panel accordingly
457 private void prepareSecondPanel()
459 DelimiterInfo info = getSelectedDelimiterInfo();
460 FileSplitter splitter = new FileSplitter(_fileCacher);
461 // Check info makes sense - num fields > 0, num records > 0
462 // set "Finished" button to disabled if not ok
463 // TODO: Work out if there are header rows or not, save?
464 // Try to match header rows with fields
465 // Try to match data with fields
466 // Add data to GUI elements
467 Object[][] tableData = splitter.splitFieldData(info.getDelimiter());
468 // possible to ignore blank columns here
469 _currentDelimiter = info.getDelimiter();
470 _fileExtractTableModel.updateData(tableData);
471 _fieldTableModel = new FieldSelectionTableModel();
473 // Check number of fields and use last ones if count matches
474 Field[] startFieldArray = null;
475 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
476 startFieldArray = _lastSelectedFields;
478 startFieldArray = splitter.makeDefaultFields();
479 _fieldTableModel.updateData(startFieldArray);
480 _fieldTable.setModel(_fieldTableModel);
481 // add dropdowns to second column
482 JComboBox fieldTypesBox = new JComboBox();
483 for (int i=0; i<Field.ALL_AVAILABLE_FIELDS.length; i++)
485 fieldTypesBox.addItem(Field.ALL_AVAILABLE_FIELDS[i].getName());
487 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
489 // Set altitude format to same as last time if available
490 if (_lastAltitudeFormat == Altitude.FORMAT_METRES)
491 _unitsDropDown.setSelectedIndex(0);
492 else if (_lastAltitudeFormat == Altitude.FORMAT_FEET)
493 _unitsDropDown.setSelectedIndex(1);
494 // no selection on field list
500 * All options have been selected, so load file
502 private void finished()
504 // Save delimiter, field array and altitude format for later use
505 _lastUsedDelimiter = _currentDelimiter;
506 _lastSelectedFields = _fieldTableModel.getFieldArray();
507 int altitudeFormat = Altitude.FORMAT_METRES;
508 if (_unitsDropDown.getSelectedIndex() == 1)
510 altitudeFormat = Altitude.FORMAT_FEET;
512 _lastAltitudeFormat = altitudeFormat;
514 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
515 _fileExtractTableModel.getData(), altitudeFormat,
517 // clear up file cacher
525 * Make a panel with a label and a component
526 * @param inLabelKey label key to use
527 * @param inComponent component for main area of panel
528 * @return labelled Panel
530 private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
532 JPanel panel = new JPanel();
533 panel.setLayout(new BorderLayout());
534 panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
535 panel.add(inComponent, BorderLayout.CENTER);
541 * An entry in the field list has been selected
542 * @param inFieldNum index of field, starting with 0
544 private void selectField(int inFieldNum)
546 if (inFieldNum == -1 || inFieldNum != _selectedField)
548 _selectedField = inFieldNum;
549 _moveUpButton.setEnabled(inFieldNum > 0);
550 _moveDownButton.setEnabled(inFieldNum >= 0
551 && inFieldNum < (_fieldTableModel.getRowCount()-1));
557 * @return the last delimiter character used for a load
559 public char getLastUsedDelimiter()
561 return _lastUsedDelimiter;