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;
11 import java.awt.event.WindowAdapter;
12 import java.awt.event.WindowEvent;
15 import javax.swing.event.DocumentEvent;
16 import javax.swing.event.DocumentListener;
17 import javax.swing.event.ListSelectionEvent;
18 import javax.swing.event.ListSelectionListener;
19 import javax.swing.table.TableCellEditor;
24 import tim.prune.I18nManager;
25 import tim.prune.data.Altitude;
26 import tim.prune.data.Field;
27 import tim.prune.data.SourceInfo;
31 * Class to handle loading of text files including GUI options,
32 * and passing loaded data back to App object
34 public class TextFileLoader
36 private File _file = null;
37 private App _app = null;
38 private JFrame _parentFrame = null;
39 private JDialog _dialog = null;
40 private JPanel _cardPanel = null;
41 private CardLayout _layout = null;
42 private JButton _backButton = null, _nextButton = null;
43 private JButton _finishButton = null;
44 private JButton _moveUpButton = null, _moveDownButton = null;
45 private JRadioButton[] _delimiterRadios = null;
46 private JTextField _otherDelimiterText = null;
47 private JLabel _statusLabel = null;
48 private DelimiterInfo[] _delimiterInfos = null;
49 private FileCacher _fileCacher = null;
50 private JList _snippetBox = null;
51 private FileExtractTableModel _fileExtractTableModel = null;
52 private JTable _fieldTable;
53 private FieldSelectionTableModel _fieldTableModel = null;
54 private JComboBox _unitsDropDown = null;
55 private int _selectedField = -1;
56 private char _currentDelimiter = ',';
58 // previously selected values
59 private char _lastUsedDelimiter = ',';
60 private Field[] _lastSelectedFields = null;
61 private Altitude.Format _lastAltitudeFormat = Altitude.Format.NO_FORMAT;
64 private static final int SNIPPET_SIZE = 6;
65 private static final int MAX_SNIPPET_WIDTH = 80;
66 private static final char[] DELIMITERS = {',', '\t', ';', ' '};
70 * Inner class to listen for delimiter change operations
72 private class DelimListener implements ActionListener, DocumentListener
74 public void actionPerformed(ActionEvent e)
76 informDelimiterSelected();
78 public void changedUpdate(DocumentEvent e)
80 informDelimiterSelected();
82 public void insertUpdate(DocumentEvent e)
84 informDelimiterSelected();
86 public void removeUpdate(DocumentEvent e)
88 informDelimiterSelected();
95 * @param inApp Application object to inform of track load
96 * @param inParentFrame parent frame to reference for dialogs
98 public TextFileLoader(App inApp, JFrame inParentFrame)
101 _parentFrame = inParentFrame;
106 * Open the selected file and show the GUI dialog to select load options
107 * @param inFile file to open
109 public void openFile(File inFile)
112 if (preCheckFile(_file))
114 _dialog = new JDialog(_parentFrame, I18nManager.getText("dialog.openoptions.title"), true);
115 _dialog.setLocationRelativeTo(_parentFrame);
116 _dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
117 // add closing listener
118 _dialog.addWindowListener(new WindowAdapter() {
119 public void windowClosing(WindowEvent e) {
121 _app.informNoDataLoaded();
124 _dialog.getContentPane().add(makeDialogComponents());
126 // select best separator according to row counts (more is better)
127 int bestDelim = getBestOption(_delimiterInfos[0].getNumWinningRecords(),
128 _delimiterInfos[1].getNumWinningRecords(), _delimiterInfos[2].getNumWinningRecords(),
129 _delimiterInfos[3].getNumWinningRecords());
131 _delimiterRadios[bestDelim].setSelected(true);
133 _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
134 informDelimiterSelected();
136 _dialog.setVisible(true);
139 // Didn't pass pre-check
140 _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
141 _app.informNoDataLoaded();
147 * Check the given file for readability and funny characters,
148 * and count the fields for the various separators
149 * @param inFile file to check
151 private boolean preCheckFile(File inFile)
153 // Check file exists and is readable
154 if (inFile == null || !inFile.exists() || !inFile.canRead())
158 // Use a FileCacher to read the file into an array
159 _fileCacher = new FileCacher(inFile);
161 // Check each line of the file
162 String[] fileContents = _fileCacher.getContents();
163 boolean fileOK = true;
164 _delimiterInfos = new DelimiterInfo[5];
165 for (int i=0; i<4; i++) _delimiterInfos[i] = new DelimiterInfo(DELIMITERS[i]);
167 String currLine = null;
168 String[] splitFields = null;
169 int commaFields = 0, semicolonFields = 0, tabFields = 0, spaceFields = 0;
170 for (int lineNum=0; lineNum<fileContents.length && fileOK; lineNum++)
172 currLine = fileContents[lineNum];
173 // check for invalid characters
174 if (currLine.indexOf('\0') >= 0) {fileOK = false;}
176 splitFields = currLine.split(",");
177 commaFields = splitFields.length;
178 if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
179 _delimiterInfos[0].updateMaxFields(commaFields);
181 splitFields = currLine.split("\t");
182 tabFields = splitFields.length;
183 if (tabFields > 1) _delimiterInfos[1].incrementNumRecords();
184 _delimiterInfos[1].updateMaxFields(tabFields);
185 // check for semicolons
186 splitFields = currLine.split(";");
187 semicolonFields = splitFields.length;
188 if (semicolonFields > 1) _delimiterInfos[2].incrementNumRecords();
189 _delimiterInfos[2].updateMaxFields(semicolonFields);
191 splitFields = currLine.split(" ");
192 spaceFields = splitFields.length;
193 if (spaceFields > 1) _delimiterInfos[3].incrementNumRecords();
194 _delimiterInfos[3].updateMaxFields(spaceFields);
195 // increment counters
196 int bestScorer = getBestOption(commaFields, tabFields, semicolonFields, spaceFields);
198 _delimiterInfos[bestScorer].incrementNumWinningRecords();
205 * Get the index of the best one in the list
206 * @return the index of the maximum of the four given values
208 private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
212 if (inOpt0 > maxScore) {bestIndex = 0; maxScore = inOpt0;}
213 if (inOpt1 > maxScore) {bestIndex = 1; maxScore = inOpt1;}
214 if (inOpt2 > maxScore) {bestIndex = 2; maxScore = inOpt2;}
215 if (inOpt3 > maxScore) {bestIndex = 3; maxScore = inOpt3;}
221 * Make the components for the open options dialog
222 * @return Component for all options
224 private Component makeDialogComponents()
226 JPanel wholePanel = new JPanel();
227 wholePanel.setLayout(new BorderLayout());
229 // add buttons to south
230 JPanel buttonPanel = new JPanel();
231 buttonPanel.setLayout(new FlowLayout(FlowLayout.CENTER));
232 _backButton = new JButton(I18nManager.getText("button.back"));
233 _backButton.addActionListener(new ActionListener() {
234 public void actionPerformed(ActionEvent e)
236 _layout.previous(_cardPanel);
237 _backButton.setEnabled(false);
238 _nextButton.setEnabled(true);
239 _finishButton.setEnabled(false);
242 _backButton.setEnabled(false);
243 buttonPanel.add(_backButton);
244 _nextButton = new JButton(I18nManager.getText("button.next"));
245 _nextButton.addActionListener(new ActionListener() {
246 public void actionPerformed(ActionEvent e)
248 prepareSecondPanel();
249 _layout.next(_cardPanel);
250 _nextButton.setEnabled(false);
251 _backButton.setEnabled(true);
252 _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1);
255 buttonPanel.add(_nextButton);
256 _finishButton = new JButton(I18nManager.getText("button.finish"));
257 _finishButton.addActionListener(new ActionListener() {
258 public void actionPerformed(ActionEvent e)
263 _finishButton.setEnabled(false);
264 buttonPanel.add(_finishButton);
265 JButton cancelButton = new JButton(I18nManager.getText("button.cancel"));
266 cancelButton.addActionListener(new ActionListener() {
267 public void actionPerformed(ActionEvent e)
270 _app.informNoDataLoaded();
273 buttonPanel.add(cancelButton);
274 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
276 // Make the two cards, for delimiter and fields
277 _cardPanel = new JPanel();
278 _layout = new CardLayout();
279 _cardPanel.setLayout(_layout);
280 JPanel firstCard = new JPanel();
281 firstCard.setLayout(new BorderLayout());
282 firstCard.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
284 JPanel delimsPanel = new JPanel();
285 delimsPanel.setLayout(new GridLayout(0, 2));
286 delimsPanel.add(new JLabel(I18nManager.getText("dialog.delimiter.label")));
287 delimsPanel.add(new JLabel("")); // blank label to go to next grid row
289 _delimiterRadios = new JRadioButton[5];
290 _delimiterRadios[0] = new JRadioButton(I18nManager.getText("dialog.delimiter.comma"));
291 delimsPanel.add(_delimiterRadios[0]);
292 _delimiterRadios[1] = new JRadioButton(I18nManager.getText("dialog.delimiter.tab"));
293 delimsPanel.add(_delimiterRadios[1]);
294 _delimiterRadios[2] = new JRadioButton(I18nManager.getText("dialog.delimiter.semicolon"));
295 delimsPanel.add(_delimiterRadios[2]);
296 _delimiterRadios[3] = new JRadioButton(I18nManager.getText("dialog.delimiter.space"));
297 delimsPanel.add(_delimiterRadios[3]);
298 JPanel otherPanel = new JPanel();
299 otherPanel.setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
300 _delimiterRadios[4] = new JRadioButton(I18nManager.getText("dialog.delimiter.other"));
301 otherPanel.add(_delimiterRadios[4]);
302 _otherDelimiterText = new JTextField(new OneCharDocument(), null, 2);
303 otherPanel.add(_otherDelimiterText);
304 // Group radio buttons
305 ButtonGroup delimGroup = new ButtonGroup();
306 DelimListener delimListener = new DelimListener();
307 for (int i=0; i<_delimiterRadios.length; i++)
309 delimGroup.add(_delimiterRadios[i]);
310 _delimiterRadios[i].addActionListener(delimListener);
312 _otherDelimiterText.getDocument().addDocumentListener(delimListener);
313 delimsPanel.add(new JLabel(""));
314 delimsPanel.add(otherPanel);
315 _statusLabel = new JLabel("");
316 delimsPanel.add(_statusLabel);
317 firstCard.add(delimsPanel, BorderLayout.SOUTH);
318 // load snippet to show first few lines
319 _snippetBox = new JList(_fileCacher.getSnippet(SNIPPET_SIZE, MAX_SNIPPET_WIDTH));
320 _snippetBox.setEnabled(false);
321 firstCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", _snippetBox), BorderLayout.CENTER);
323 // Second screen, for field order selection
324 JPanel secondCard = new JPanel();
325 secondCard.setLayout(new BorderLayout());
326 secondCard.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 15));
327 // table for file contents
328 _fileExtractTableModel = new FileExtractTableModel();
329 JTable extractTable = new JTable(_fileExtractTableModel);
330 JScrollPane tableScrollPane = new JScrollPane(extractTable);
331 extractTable.setPreferredScrollableViewportSize(new Dimension(350, 80));
332 extractTable.getTableHeader().setReorderingAllowed(false);
333 secondCard.add(makeLabelledPanel("dialog.openoptions.filesnippet", tableScrollPane), BorderLayout.NORTH);
334 JPanel innerPanel2 = new JPanel();
335 innerPanel2.setLayout(new BorderLayout());
336 innerPanel2.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
338 _fieldTable = new JTable(new FieldSelectionTableModel());
339 _fieldTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
340 // add listener for selected table row
341 _fieldTable.getSelectionModel().addListSelectionListener(
342 new ListSelectionListener() {
343 public void valueChanged(ListSelectionEvent e) {
344 ListSelectionModel lsm = (ListSelectionModel) e.getSource();
345 if (lsm.isSelectionEmpty()) {
346 //no rows are selected
349 selectField(lsm.getMinSelectionIndex());
353 JScrollPane lowerTablePane = new JScrollPane(_fieldTable);
354 lowerTablePane.setPreferredSize(new Dimension(300, 100));
355 innerPanel2.add(lowerTablePane, BorderLayout.CENTER);
357 JPanel innerPanel3 = new JPanel();
358 innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
359 _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
360 _moveUpButton.addActionListener(new ActionListener() {
361 public void actionPerformed(ActionEvent e)
363 int currRow = _fieldTable.getSelectedRow();
364 closeTableComboBox(currRow);
365 _fieldTableModel.moveUp(currRow);
366 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
369 innerPanel3.add(_moveUpButton);
370 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
371 _moveDownButton.addActionListener(new ActionListener() {
372 public void actionPerformed(ActionEvent e)
374 int currRow = _fieldTable.getSelectedRow();
375 closeTableComboBox(currRow);
376 _fieldTableModel.moveDown(currRow);
377 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
380 innerPanel3.add(_moveDownButton);
381 innerPanel3.add(Box.createVerticalStrut(60));
382 JButton guessButton = new JButton(I18nManager.getText("button.guessfields"));
383 guessButton.addActionListener(new ActionListener() {
384 public void actionPerformed(ActionEvent e)
386 _lastSelectedFields = null;
387 prepareSecondPanel();
390 innerPanel3.add(guessButton);
392 innerPanel2.add(innerPanel3, BorderLayout.EAST);
393 secondCard.add(innerPanel2, BorderLayout.CENTER);
394 JPanel altUnitsPanel = new JPanel();
395 altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
396 altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
397 String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
398 _unitsDropDown = new JComboBox(units);
399 altUnitsPanel.add(_unitsDropDown);
400 secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
401 _cardPanel.add(firstCard, "card1");
402 _cardPanel.add(secondCard, "card2");
404 wholePanel.add(_cardPanel, BorderLayout.CENTER);
410 * Close the combo box on the selected row of the field table
411 * @param inRow currently selected row number
413 private void closeTableComboBox(int inRow)
415 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
418 editor.stopCellEditing();
424 * change the status based on selection of a delimiter
426 protected void informDelimiterSelected()
429 // Loop through radios to see which one is selected
430 for (int i=0; i<(_delimiterRadios.length-1); i++)
432 if (_delimiterRadios[i].isSelected())
434 // Set label text to describe records and fields
435 int numRecords = _delimiterInfos[i].getNumRecords();
438 _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
442 fields = _delimiterInfos[i].getMaxFields();
443 _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
444 + " " + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
448 // Don't show label if "other" delimiter is chosen (as records, fields are unknown)
449 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
451 _statusLabel.setText("");
453 // enable/disable next button
454 _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1)
455 || _otherDelimiterText.getText().length() == 1);
460 * Get the delimiter info from the first step
461 * @return delimiter information object for the selected delimiter
463 public DelimiterInfo getSelectedDelimiterInfo()
465 for (int i=0; i<4; i++)
466 if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
467 // must be "other" - build info if necessary
468 if (_delimiterInfos[4] == null)
469 _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
470 return _delimiterInfos[4];
475 * Use the delimiter selected to determine the fields in the file
476 * and prepare the second panel accordingly
478 private void prepareSecondPanel()
480 DelimiterInfo info = getSelectedDelimiterInfo();
481 FileSplitter splitter = new FileSplitter(_fileCacher);
482 // Check info makes sense - num fields > 0, num records > 0
483 // set "Finished" button to disabled if not ok
484 // Add data to GUI elements
485 String[][] tableData = splitter.splitFieldData(info.getDelimiter());
486 // possible to ignore blank columns here
487 _currentDelimiter = info.getDelimiter();
488 _fileExtractTableModel.updateData(tableData);
489 _fieldTableModel = new FieldSelectionTableModel();
491 // Check number of fields and use last ones if count matches
492 Field[] startFieldArray = null;
493 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
495 startFieldArray = _lastSelectedFields;
499 // Take first full row of file and use it to guess fields
500 startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
503 _fieldTableModel.updateData(startFieldArray);
504 _fieldTable.setModel(_fieldTableModel);
505 // add dropdowns to second column
506 JComboBox fieldTypesBox = new JComboBox();
507 String[] fieldNames = Field.getFieldNames();
508 for (int i=0; i<fieldNames.length; i++)
510 fieldTypesBox.addItem(fieldNames[i]);
512 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
514 // Set altitude format to same as last time if available
515 if (_lastAltitudeFormat == Altitude.Format.METRES)
516 _unitsDropDown.setSelectedIndex(0);
517 else if (_lastAltitudeFormat == Altitude.Format.FEET)
518 _unitsDropDown.setSelectedIndex(1);
519 // no selection on field list
525 * All options have been selected, so load file
527 private void finished()
529 // Save delimiter, field array and altitude format for later use
530 _lastUsedDelimiter = _currentDelimiter;
531 _lastSelectedFields = _fieldTableModel.getFieldArray();
532 Altitude.Format altitudeFormat = Altitude.Format.METRES;
533 if (_unitsDropDown.getSelectedIndex() == 1)
535 altitudeFormat = Altitude.Format.FEET;
537 _lastAltitudeFormat = altitudeFormat;
539 SourceInfo sourceInfo = new SourceInfo(_file, SourceInfo.FILE_TYPE.TEXT);
540 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
541 _fileExtractTableModel.getData(), altitudeFormat, sourceInfo);
542 // clear up file cacher
550 * Make a panel with a label and a component
551 * @param inLabelKey label key to use
552 * @param inComponent component for main area of panel
553 * @return labelled Panel
555 private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
557 JPanel panel = new JPanel();
558 panel.setLayout(new BorderLayout());
559 panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
560 panel.add(inComponent, BorderLayout.CENTER);
566 * An entry in the field list has been selected
567 * @param inFieldNum index of field, starting with 0
569 private void selectField(int inFieldNum)
571 if (inFieldNum == -1 || inFieldNum != _selectedField)
573 _selectedField = inFieldNum;
574 _moveUpButton.setEnabled(inFieldNum > 0);
575 _moveDownButton.setEnabled(inFieldNum >= 0
576 && inFieldNum < (_fieldTableModel.getRowCount()-1));
582 * @return the last delimiter character used for a load
584 public char getLastUsedDelimiter()
586 return _lastUsedDelimiter;