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 * Class to handle loading of text files including GUI options,
28 * and passing loaded data back to App object
30 public class TextFileLoader
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 FileCacher _fileCacher = null;
46 private JList _snippetBox = null;
47 private FileExtractTableModel _fileExtractTableModel = null;
48 private JTable _fieldTable;
49 private FieldSelectionTableModel _fieldTableModel = null;
50 private JComboBox _unitsDropDown = null;
51 private int _selectedField = -1;
52 private char _currentDelimiter = ',';
54 // previously selected values
55 private char _lastUsedDelimiter = ',';
56 private Field[] _lastSelectedFields = null;
57 private Altitude.Format _lastAltitudeFormat = Altitude.Format.NO_FORMAT;
60 private static final int SNIPPET_SIZE = 6;
61 private static final int MAX_SNIPPET_WIDTH = 80;
62 private static final char[] DELIMITERS = {',', '\t', ';', ' '};
66 * Inner class to listen for delimiter change operations
68 private class DelimListener implements ActionListener, DocumentListener
70 public void actionPerformed(ActionEvent e)
72 informDelimiterSelected();
74 public void changedUpdate(DocumentEvent e)
76 informDelimiterSelected();
78 public void insertUpdate(DocumentEvent e)
80 informDelimiterSelected();
82 public void removeUpdate(DocumentEvent e)
84 informDelimiterSelected();
91 * @param inApp Application object to inform of track load
92 * @param inParentFrame parent frame to reference for dialogs
94 public TextFileLoader(App inApp, JFrame inParentFrame)
97 _parentFrame = inParentFrame;
102 * Open the selected file and show the GUI dialog to select load options
103 * @param inFile file to open
105 public void openFile(File inFile)
108 if (preCheckFile(_file))
110 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
111 _dialog.setLocationRelativeTo(_parentFrame);
112 _dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
113 _dialog.getContentPane().add(makeDialogComponents());
115 // select best separator according to row counts (more is better)
116 int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
117 _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
118 _delimiterInfos[3].getNumWinningRecords());
120 _delimiterRadios[bestDelim].setSelected(true);
122 _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
123 informDelimiterSelected();
125 _dialog.setVisible(true);
128 // Didn't pass pre-check
129 _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
130 _app.informNoDataLoaded();
136 * Check the given file for readability and funny characters,
137 * and count the fields for the various separators
138 * @param inFile file to check
140 private boolean preCheckFile(File inFile)
142 // Check file exists and is readable
143 if (inFile == null || !inFile.exists() || !inFile.canRead())
147 // Use a FileCacher to read the file into an array
148 _fileCacher = new FileCacher(inFile);
150 // Check each line of the file
151 String[] fileContents = _fileCacher.getContents();
152 boolean fileOK = true;
153 _delimiterInfos = new DelimiterInfo[5];
154 for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
156 String currLine = null;
157 String[] splitFields = null;
158 int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
159 for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
161 currLine = fileContents[lineNum];
162 // check for invalid characters
163 if (currLine.indexOf('\0') >= 0) {fileOK = false;}
165 splitFields = currLine.split(",");
166 commaFields = splitFields.length;
167 if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
168 _delimiterInfos[0].updateMaxFields(commaFields);
170 splitFields = currLine.split("\t");
171 tabFields = splitFields.length;
172 if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
173 _delimiterInfos[1].updateMaxFields(tabFields);
174 // check for semicolons
175 splitFields = currLine.split(";");
176 semicolonFields = splitFields.length;
177 if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
178 _delimiterInfos[2].updateMaxFields(semicolonFields);
180 splitFields = currLine.split(" ");
181 spaceFields = splitFields.length;
182 if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
183 _delimiterInfos[3].updateMaxFields(spaceFields);
184 // increment counters
185 int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
187 _delimiterInfos[bestScorer].incrementNumWinningRecords();
194 * Get the index of the best one in the list
195 * @return the index of the maximum of the four given values
197 private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
201 if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
202 if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
203 if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
204 if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
210 * Make the components for the open options dialog
211 * @return Component for all options
213 private Component makeDialogComponents()
215 JPanel wholePanel = new JPanel();
216 wholePanel.setLayout(new BorderLayout());
218 // add buttons to south
219 JPanel buttonPanel = new JPanel();
220 buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
221 _backButton = new JButton(I18nManager.getText("button.back"));
222 _backButton.addActionListener(new ActionListener() {
223 public void actionPerformed(ActionEvent e)
225 _layout.previous(_cardPanel);
226 _backButton.setEnabled(false);
227 _nextButton.setEnabled(true);
228 _finishButton.setEnabled(false);
231 _backButton.setEnabled(false);
232 buttonPanel.add(_backButton);
233 _nextButton = new JButton(I18nManager.getText("button.next"));
234 _nextButton.addActionListener(new ActionListener() {
235 public void actionPerformed(ActionEvent e)
237 prepareSecondPanel();
238 _layout.next(_cardPanel);
239 _nextButton.setEnabled(false);
240 _backButton.setEnabled(true);
241 _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1);
244 buttonPanel.add(_nextButton);
245 _finishButton = new JButton(I18nManager.getText("button.finish"));
246 _finishButton.addActionListener(new ActionListener() {
247 public void actionPerformed(ActionEvent e)
252 _finishButton.setEnabled(false);
253 buttonPanel.add(_finishButton);
254 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
255 cancelButton.addActionListener(new ActionListener() {
256 public void actionPerformed(ActionEvent e)
259 _app.informNoDataLoaded();
262 buttonPanel.add(cancelButton);
263 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
265 // Make the two cards, for delimiter and fields
266 _cardPanel = new JPanel();
267 _layout = new CardLayout();
268 _cardPanel.setLayout(_layout);
269 JPanel firstCard = new JPanel();
270 firstCard.setLayout(new BorderLayout());
271 firstCard.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
273 JPanel delimsPanel = new JPanel();
274 delimsPanel.setLayout(new GridLayout(0, 2));
275 delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
276 delimsPanel.add(new JLabel("")); // blank label to go to next grid row
278 _delimiterRadios = new JRadioButton[5];
279 _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
280 delimsPanel.add(_delimiterRadios[0]);
281 _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
282 delimsPanel.add(_delimiterRadios[1]);
283 _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
284 delimsPanel.add(_delimiterRadios[2]);
285 _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
286 delimsPanel.add(_delimiterRadios[3]);
287 JPanel otherPanel = new JPanel();
288 otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
289 _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
290 otherPanel.add(_delimiterRadios[4]);
291 _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
292 otherPanel.add(_otherDelimiterText);
293 // Group radio buttons
294 ButtonGroup delimGroup = new ButtonGroup();
295 DelimListener delimListener = new DelimListener();
296 for (int i=0; i<_delimiterRadios.length; i++)
298 delimGroup.add(_delimiterRadios[i]);
299 _delimiterRadios[i].addActionListener(delimListener);
301 _otherDelimiterText.getDocument().addDocumentListener(delimListener);
302 delimsPanel.add(new JLabel(""));
303 delimsPanel.add(otherPanel);
304 _statusLabel = new JLabel("");
305 delimsPanel.add(_statusLabel);
306 firstCard.add(delimsPanel, BorderLayout.SOUTH);
307 // load snippet to show first few lines
308 _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
309 _snippetBox.setEnabled(false);
310 firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
312 // Second screen, for field order selection
313 JPanel secondCard = new JPanel();
314 secondCard.setLayout(new BorderLayout());
315 secondCard.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
316 // table for file contents
317 _fileExtractTableModel = new FileExtractTableModel();
318 JTable extractTable = new JTable(_fileExtractTableModel);
319 JScrollPane tableScrollPane = new JScrollPane(extractTable);
320 extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
321 extractTable.getTableHeader().setReorderingAllowed(false);
322 secondCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", tableScrollPane), BorderLayout.NORTH);
323 JPanel innerPanel2 = new JPanel();
324 innerPanel2.setLayout(new BorderLayout());
325 innerPanel2.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
327 _fieldTable = new JTable(new FieldSelectionTableModel());
328 _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
329 // add listener for selected table row
330 _fieldTable.getSelectionModel().addListSelectionListener(
331 new ListSelectionListener() {
332 public void valueChanged(ListSelectionEvent e) {
333 ListSelectionModel lsm = (ListSelectionModel) e.getSource();
334 if (lsm.isSelectionEmpty()) {
335 //no rows are selected
338 selectField(lsm.getMinSelectionIndex());
342 JPanel tablePanel = new JPanel();
343 tablePanel.setLayout(new BorderLayout());
344 tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
345 tablePanel.add(_fieldTable, BorderLayout.CENTER);
346 innerPanel2.add(tablePanel, BorderLayout.CENTER);
348 JPanel innerPanel3 = new JPanel();
349 innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
350 _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
351 _moveUpButton.addActionListener(new ActionListener() {
352 public void actionPerformed(ActionEvent e)
354 int currRow = _fieldTable.getSelectedRow();
355 closeTableComboBox(currRow);
356 _fieldTableModel.moveUp(currRow);
357 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
360 innerPanel3.add(_moveUpButton);
361 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
362 _moveDownButton.addActionListener(new ActionListener() {
363 public void actionPerformed(ActionEvent e)
365 int currRow = _fieldTable.getSelectedRow();
366 closeTableComboBox(currRow);
367 _fieldTableModel.moveDown(currRow);
368 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
371 innerPanel3.add(_moveDownButton);
372 innerPanel3.add(Box.createVerticalStrut(60));
373 JButton guessButton = new JButton(I18nManager.getText("button.guessfields"));
374 guessButton.addActionListener(new ActionListener() {
375 public void actionPerformed(ActionEvent e)
377 _lastSelectedFields = null;
378 prepareSecondPanel();
381 innerPanel3.add(guessButton);
383 innerPanel2.add(innerPanel3, BorderLayout.EAST);
384 secondCard.add(innerPanel2, BorderLayout.CENTER);
385 JPanel altUnitsPanel = new JPanel();
386 altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
387 altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
388 String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
389 _unitsDropDown = new JComboBox(units);
390 altUnitsPanel.add(_unitsDropDown);
391 secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
392 _cardPanel.add(firstCard, "card1");
393 _cardPanel.add(secondCard, "card2");
395 wholePanel.add(_cardPanel, BorderLayout.CENTER);
401 * Close the combo box on the selected row of the field table
402 * @param inRow currently selected row number
404 private void closeTableComboBox(int inRow)
406 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
409 editor.stopCellEditing();
415 * change the status based on selection of a delimiter
417 protected void informDelimiterSelected()
420 // Loop through radios to see which one is selected
421 for (int i=0; i<(_delimiterRadios.length-1); i++)
423 if (_delimiterRadios[i].isSelected())
425 // Set label text to describe records and fields
426 int numRecords = _delimiterInfos[i].getNumRecords();
429 _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
433 fields = _delimiterInfos[i].getMaxFields();
434 _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
435 + " " + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
439 // Don't show label if "other" delimiter is chosen (as records, fields are unknown)
440 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
442 _statusLabel.setText("");
444 // enable/disable next button
445 _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1)
446 || _otherDelimiterText.getText().length() == 1);
451 * Get the delimiter info from the first step
452 * @return delimiter information object for the selected delimiter
454 public DelimiterInfo getSelectedDelimiterInfo()
456 for (int i=0; i<4; i++)
457 if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
458 // must be "other" - build info if necessary
459 if (_delimiterInfos[4] == null)
460 _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
461 return _delimiterInfos[4];
466 * Use the delimiter selected to determine the fields in the file
467 * and prepare the second panel accordingly
469 private void prepareSecondPanel()
471 DelimiterInfo info = getSelectedDelimiterInfo();
472 FileSplitter splitter = new FileSplitter(_fileCacher);
473 // Check info makes sense - num fields > 0, num records > 0
474 // set "Finished" button to disabled if not ok
475 // Add data to GUI elements
476 String[][] tableData = splitter.splitFieldData(info.getDelimiter());
477 // possible to ignore blank columns here
478 _currentDelimiter = info.getDelimiter();
479 _fileExtractTableModel.updateData(tableData);
480 _fieldTableModel = new FieldSelectionTableModel();
482 // Check number of fields and use last ones if count matches
483 Field[] startFieldArray = null;
484 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
486 startFieldArray = _lastSelectedFields;
490 // Take first full row of file and use it to guess fields
491 startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
494 _fieldTableModel.updateData(startFieldArray);
495 _fieldTable.setModel(_fieldTableModel);
496 // add dropdowns to second column
497 JComboBox fieldTypesBox = new JComboBox();
498 String[] fieldNames = Field.getFieldNames();
499 for (int i=0; i<fieldNames.length; i++)
501 fieldTypesBox.addItem(fieldNames[i]);
503 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
505 // Set altitude format to same as last time if available
506 if (_lastAltitudeFormat == Altitude.Format.METRES)
507 _unitsDropDown.setSelectedIndex(0);
508 else if (_lastAltitudeFormat == Altitude.Format.FEET)
509 _unitsDropDown.setSelectedIndex(1);
510 // no selection on field list
516 * All options have been selected, so load file
518 private void finished()
520 // Save delimiter, field array and altitude format for later use
521 _lastUsedDelimiter = _currentDelimiter;
522 _lastSelectedFields = _fieldTableModel.getFieldArray();
523 Altitude.Format altitudeFormat = Altitude.Format.METRES;
524 if (_unitsDropDown.getSelectedIndex() == 1)
526 altitudeFormat = Altitude.Format.FEET;
528 _lastAltitudeFormat = altitudeFormat;
530 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
531 _fileExtractTableModel.getData(), altitudeFormat,
533 // clear up file cacher
541 * Make a panel with a label and a component
542 * @param inLabelKey label key to use
543 * @param inComponent component for main area of panel
544 * @return labelled Panel
546 private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
548 JPanel panel = new JPanel();
549 panel.setLayout(new BorderLayout());
550 panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
551 panel.add(inComponent, BorderLayout.CENTER);
557 * An entry in the field list has been selected
558 * @param inFieldNum index of field, starting with 0
560 private void selectField(int inFieldNum)
562 if (inFieldNum == -1 || inFieldNum != _selectedField)
564 _selectedField = inFieldNum;
565 _moveUpButton.setEnabled(inFieldNum > 0);
566 _moveDownButton.setEnabled(inFieldNum >= 0
567 && inFieldNum < (_fieldTableModel.getRowCount()-1));
573 * @return the last delimiter character used for a load
575 public char getLastUsedDelimiter()
577 return _lastUsedDelimiter;