]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/load/TextFileLoader.java
Version 8, September 2009
[GpsPrune.git] / tim / prune / load / TextFileLoader.java
1 package tim.prune.load;
2
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 javax.swing.*;
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;
17
18 import java.io.File;
19
20 import tim.prune.App;
21 import tim.prune.I18nManager;
22 import tim.prune.data.Altitude;
23 import tim.prune.data.Field;
24
25
26 /**
27  * Class to handle loading of text files including GUI options,
28  * and passing loaded data back to App object
29  */
30 public class TextFileLoader
31 {
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 = ',';
53
54         // previously selected values
55         private char _lastUsedDelimiter = ',';
56         private Field[] _lastSelectedFields = null;
57         private Altitude.Format _lastAltitudeFormat = Altitude.Format.NO_FORMAT;
58
59         // constants
60         private static final int SNIPPET_SIZE = 6;
61         private static final int MAX_SNIPPET_WIDTH = 80;
62         private static final char[] DELIMITERS = {',', '\t', ';', ' '};
63
64
65         /**
66          * Inner class to listen for delimiter change operations
67          */
68         private class DelimListener implements ActionListener, DocumentListener
69         {
70                 public void actionPerformed(ActionEvent e)
71                 {
72                         informDelimiterSelected();
73                 }
74                 public void changedUpdate(DocumentEvent e)
75                 {
76                         informDelimiterSelected();
77                 }
78                 public void insertUpdate(DocumentEvent e)
79                 {
80                         informDelimiterSelected();
81                 }
82                 public void removeUpdate(DocumentEvent e)
83                 {
84                         informDelimiterSelected();
85                 }
86         }
87
88
89         /**
90          * Constructor
91          * @param inApp Application object to inform of track load
92          * @param inParentFrame parent frame to reference for dialogs
93          */
94         public TextFileLoader(App inApp, JFrame inParentFrame)
95         {
96                 _app = inApp;
97                 _parentFrame = inParentFrame;
98         }
99
100
101         /**
102          * Open the selected file and show the GUI dialog to select load options
103          * @param inFile file to open
104          */
105         public void openFile(File inFile)
106         {
107                 _file = inFile;
108                 if (preCheckFile(_file))
109                 {
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());
114
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());
119                         if (bestDelim >= 0)
120                                 _delimiterRadios[bestDelim].setSelected(true);
121                         else
122                                 _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
123                         informDelimiterSelected();
124                         _dialog.pack();
125                         _dialog.setVisible(true);
126                 }
127                 else {
128                         // Didn't pass pre-check
129                         _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
130                         _app.informNoDataLoaded();
131                 }
132         }
133
134
135         /**
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
139          */
140         private boolean preCheckFile(File inFile)
141         {
142                 // Check file exists and is readable
143                 if (inFile == null || !inFile.exists() || !inFile.canRead())
144                 {
145                         return false;
146                 }
147                 // Use a FileCacher to read the file into an array
148                 _fileCacher = new FileCacher(inFile);
149
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]);
155
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++)
160                 {
161                         currLine = fileContents[lineNum];
162                         // check for invalid characters
163                         if (currLine.indexOf('\0') >= 0) {fileOK = false;}
164                         // check for commas
165                         splitFields = currLine.split(",");
166                         commaFields = splitFields.length;
167                         if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
168                         _delimiterInfos[0].updateMaxFields(commaFields);
169                         // check for tabs
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);
179                         // check for spaces
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);
186                         if (bestScorer >= 0)
187                                 _delimiterInfos[bestScorer].incrementNumWinningRecords();
188                 }
189                 return fileOK;
190         }
191
192
193         /**
194          * Get the index of the best one in the list
195          * @return the index of the maximum of the four given values
196          */
197         private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
198         {
199                 int bestIndex = -1;
200                 int maxScore = 1;
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;}
205                 return bestIndex;
206         }
207
208
209         /**
210          * Make the components for the open options dialog
211          * @return Component for all options
212          */
213         private Component makeDialogComponents()
214         {
215                 JPanel wholePanel = new JPanel();
216                 wholePanel.setLayout(new BorderLayout());
217
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)
224                         {
225                                 _layout.previous(_cardPanel);
226                                 _backButton.setEnabled(false);
227                                 _nextButton.setEnabled(true);
228                                 _finishButton.setEnabled(false);
229                         }
230                 });
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)
236                         {
237                                 prepareSecondPanel();
238                                 _layout.next(_cardPanel);
239                                 _nextButton.setEnabled(false);
240                                 _backButton.setEnabled(true);
241                                 _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1);
242                         }
243                 });
244                 buttonPanel.add(_nextButton);
245                 _finishButton = new JButton(I18nManager.getText("button.finish"));
246                 _finishButton.addActionListener(new ActionListener() {
247                         public void actionPerformed(ActionEvent e)
248                         {
249                                 finished();
250                         }
251                 });
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)
257                         {
258                                 _dialog.dispose();
259                                 _app.informNoDataLoaded();
260                         }
261                 });
262                 buttonPanel.add(cancelButton);
263                 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
264
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));
272
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
277                 // radio buttons
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++)
297                 {
298                         delimGroup.add(_delimiterRadios[i]);
299                         _delimiterRadios[i].addActionListener(delimListener);
300                 }
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);
311
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));
326
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
336                                                 selectField(-1);
337                                         } else {
338                                                 selectField(lsm.getMinSelectionIndex());
339                                         }
340                                 }
341                         });
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);
347
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)
353                         {
354                                 int currRow = _fieldTable.getSelectedRow();
355                                 closeTableComboBox(currRow);
356                                 _fieldTableModel.moveUp(currRow);
357                                 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
358                         }
359                 });
360                 innerPanel3.add(_moveUpButton);
361                 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
362                 _moveDownButton.addActionListener(new ActionListener() {
363                         public void actionPerformed(ActionEvent e)
364                         {
365                                 int currRow = _fieldTable.getSelectedRow();
366                                 closeTableComboBox(currRow);
367                                 _fieldTableModel.moveDown(currRow);
368                                 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
369                         }
370                 });
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)
376                         {
377                                 _lastSelectedFields = null;
378                                 prepareSecondPanel();
379                         }
380                 });
381                 innerPanel3.add(guessButton);
382
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");
394
395                 wholePanel.add(_cardPanel, BorderLayout.CENTER);
396                 return wholePanel;
397         }
398
399
400         /**
401          * Close the combo box on the selected row of the field table
402          * @param inRow currently selected row number
403          */
404         private void closeTableComboBox(int inRow)
405         {
406                 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
407                 if (editor != null)
408                 {
409                         editor.stopCellEditing();
410                 }
411         }
412
413
414         /**
415          * change the status based on selection of a delimiter
416          */
417         protected void informDelimiterSelected()
418         {
419                 int fields = 0;
420                 // Loop through radios to see which one is selected
421                 for (int i=0; i<(_delimiterRadios.length-1); i++)
422                 {
423                         if (_delimiterRadios[i].isSelected())
424                         {
425                                 // Set label text to describe records and fields
426                                 int numRecords = _delimiterInfos[i].getNumRecords();
427                                 if (numRecords == 0)
428                                 {
429                                         _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
430                                 }
431                                 else
432                                 {
433                                         fields = _delimiterInfos[i].getMaxFields();
434                                         _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
435                                                 + " " + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
436                                 }
437                         }
438                 }
439                 // Don't show label if "other" delimiter is chosen (as records, fields are unknown)
440                 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
441                 {
442                         _statusLabel.setText("");
443                 }
444                 // enable/disable next button
445                 _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1)
446                         || _otherDelimiterText.getText().length() == 1);
447         }
448
449
450         /**
451          * Get the delimiter info from the first step
452          * @return delimiter information object for the selected delimiter
453          */
454         public DelimiterInfo getSelectedDelimiterInfo()
455         {
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];
462         }
463
464
465         /**
466          * Use the delimiter selected to determine the fields in the file
467          * and prepare the second panel accordingly
468          */
469         private void prepareSecondPanel()
470         {
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();
481
482                 // Check number of fields and use last ones if count matches
483                 Field[] startFieldArray = null;
484                 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
485                 {
486                         startFieldArray = _lastSelectedFields;
487                 }
488                 else
489                 {
490                         // Take first full row of file and use it to guess fields
491                         startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
492                 }
493
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++)
500                 {
501                         fieldTypesBox.addItem(fieldNames[i]);
502                 }
503                 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
504
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
511                 selectField(-1);
512         }
513
514
515         /**
516          * All options have been selected, so load file
517          */
518         private void finished()
519         {
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)
525                 {
526                         altitudeFormat = Altitude.Format.FEET;
527                 }
528                 _lastAltitudeFormat = altitudeFormat;
529                 // give data to App
530                 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
531                         _fileExtractTableModel.getData(), altitudeFormat,
532                         _file.getName());
533                 // clear up file cacher
534                 _fileCacher.clear();
535                 // dispose of dialog
536                 _dialog.dispose();
537         }
538
539
540         /**
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
545          */
546         private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
547         {
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);
552                 return panel;
553         }
554
555
556         /**
557          * An entry in the field list has been selected
558          * @param inFieldNum index of field, starting with 0
559          */
560         private void selectField(int inFieldNum)
561         {
562                 if (inFieldNum == -1 || inFieldNum != _selectedField)
563                 {
564                         _selectedField = inFieldNum;
565                         _moveUpButton.setEnabled(inFieldNum > 0);
566                         _moveDownButton.setEnabled(inFieldNum >= 0
567                                 && inFieldNum < (_fieldTableModel.getRowCount()-1));
568                 }
569         }
570
571
572         /**
573          * @return the last delimiter character used for a load
574          */
575         public char getLastUsedDelimiter()
576         {
577                 return _lastUsedDelimiter;
578         }
579 }