]> gitweb.fperrin.net Git - GpsPrune.git/blob - tim/prune/load/TextFileLoader.java
Version 11, August 2010
[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 java.awt.event.WindowAdapter;
12 import java.awt.event.WindowEvent;
13
14 import javax.swing.*;
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;
20
21 import java.io.File;
22
23 import tim.prune.App;
24 import tim.prune.I18nManager;
25 import tim.prune.data.Altitude;
26 import tim.prune.data.Field;
27 import tim.prune.data.SourceInfo;
28
29
30 /**
31  * Class to handle loading of text files including GUI options,
32  * and passing loaded data back to App object
33  */
34 public class TextFileLoader
35 {
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 = ',';
57
58         // previously selected values
59         private char _lastUsedDelimiter = ',';
60         private Field[] _lastSelectedFields = null;
61         private Altitude.Format _lastAltitudeFormat = Altitude.Format.NO_FORMAT;
62
63         // constants
64         private static final int SNIPPET_SIZE = 6;
65         private static final int MAX_SNIPPET_WIDTH = 80;
66         private static final char[] DELIMITERS = {',', '\t', ';', ' '};
67
68
69         /**
70          * Inner class to listen for delimiter change operations
71          */
72         private class DelimListener implements ActionListener, DocumentListener
73         {
74                 public void actionPerformed(ActionEvent e)
75                 {
76                         informDelimiterSelected();
77                 }
78                 public void changedUpdate(DocumentEvent e)
79                 {
80                         informDelimiterSelected();
81                 }
82                 public void insertUpdate(DocumentEvent e)
83                 {
84                         informDelimiterSelected();
85                 }
86                 public void removeUpdate(DocumentEvent e)
87                 {
88                         informDelimiterSelected();
89                 }
90         }
91
92
93         /**
94          * Constructor
95          * @param inApp Application object to inform of track load
96          * @param inParentFrame parent frame to reference for dialogs
97          */
98         public TextFileLoader(App inApp, JFrame inParentFrame)
99         {
100                 _app = inApp;
101                 _parentFrame = inParentFrame;
102         }
103
104
105         /**
106          * Open the selected file and show the GUI dialog to select load options
107          * @param inFile file to open
108          */
109         public void openFile(File inFile)
110         {
111                 _file = inFile;
112                 if (preCheckFile(_file))
113                 {
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) {
120                                         _dialog.dispose();
121                                         _app.informNoDataLoaded();
122                                 }
123                         });
124                         _dialog.getContentPane().add(makeDialogComponents());
125
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());
130                         if (bestDelim >= 0)
131                                 _delimiterRadios[bestDelim].setSelected(true);
132                         else
133                                 _delimiterRadios[_delimiterRadios.length-1].setSelected(true);
134                         informDelimiterSelected();
135                         _dialog.pack();
136                         _dialog.setVisible(true);
137                 }
138                 else {
139                         // Didn't pass pre-check
140                         _app.showErrorMessage("error.load.dialogtitle", "error.load.noread");
141                         _app.informNoDataLoaded();
142                 }
143         }
144
145
146         /**
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
150          */
151         private boolean preCheckFile(File inFile)
152         {
153                 // Check file exists and is readable
154                 if (inFile == null || !inFile.exists() || !inFile.canRead())
155                 {
156                         return false;
157                 }
158                 // Use a FileCacher to read the file into an array
159                 _fileCacher = new FileCacher(inFile);
160
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]);
166
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++)
171                 {
172                         currLine = fileContents[lineNum];
173                         // check for invalid characters
174                         if (currLine.indexOf('\0') >= 0) {fileOK = false;}
175                         // check for commas
176                         splitFields = currLine.split(",");
177                         commaFields = splitFields.length;
178                         if (commaFields > 1) _delimiterInfos[0].incrementNumRecords();
179                         _delimiterInfos[0].updateMaxFields(commaFields);
180                         // check for tabs
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);
190                         // check for spaces
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);
197                         if (bestScorer >= 0)
198                                 _delimiterInfos[bestScorer].incrementNumWinningRecords();
199                 }
200                 return fileOK;
201         }
202
203
204         /**
205          * Get the index of the best one in the list
206          * @return the index of the maximum of the four given values
207          */
208         private static int getBestOption(int inOpt0, int inOpt1, int inOpt2, int inOpt3)
209         {
210                 int bestIndex = -1;
211                 int maxScore = 1;
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;}
216                 return bestIndex;
217         }
218
219
220         /**
221          * Make the components for the open options dialog
222          * @return Component for all options
223          */
224         private Component makeDialogComponents()
225         {
226                 JPanel wholePanel = new JPanel();
227                 wholePanel.setLayout(new BorderLayout());
228
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)
235                         {
236                                 _layout.previous(_cardPanel);
237                                 _backButton.setEnabled(false);
238                                 _nextButton.setEnabled(true);
239                                 _finishButton.setEnabled(false);
240                         }
241                 });
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)
247                         {
248                                 prepareSecondPanel();
249                                 _layout.next(_cardPanel);
250                                 _nextButton.setEnabled(false);
251                                 _backButton.setEnabled(true);
252                                 _finishButton.setEnabled(_fieldTableModel.getRowCount() > 1);
253                         }
254                 });
255                 buttonPanel.add(_nextButton);
256                 _finishButton = new JButton(I18nManager.getText("button.finish"));
257                 _finishButton.addActionListener(new ActionListener() {
258                         public void actionPerformed(ActionEvent e)
259                         {
260                                 finished();
261                         }
262                 });
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)
268                         {
269                                 _dialog.dispose();
270                                 _app.informNoDataLoaded();
271                         }
272                 });
273                 buttonPanel.add(cancelButton);
274                 wholePanel.add(buttonPanel, BorderLayout.SOUTH);
275
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));
283
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
288                 // radio buttons
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++)
308                 {
309                         delimGroup.add(_delimiterRadios[i]);
310                         _delimiterRadios[i].addActionListener(delimListener);
311                 }
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);
322
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));
337
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
347                                                 selectField(-1);
348                                         } else {
349                                                 selectField(lsm.getMinSelectionIndex());
350                                         }
351                                 }
352                         });
353                 JPanel tablePanel = new JPanel();
354                 tablePanel.setLayout(new BorderLayout());
355                 tablePanel.add(_fieldTable.getTableHeader(), BorderLayout.NORTH);
356                 tablePanel.add(_fieldTable, BorderLayout.CENTER);
357                 innerPanel2.add(tablePanel, BorderLayout.CENTER);
358
359                 JPanel innerPanel3 = new JPanel();
360                 innerPanel3.setLayout(new BoxLayout(innerPanel3, BoxLayout.Y_AXIS));
361                 _moveUpButton = new JButton(I18nManager.getText("button.moveup"));
362                 _moveUpButton.addActionListener(new ActionListener() {
363                         public void actionPerformed(ActionEvent e)
364                         {
365                                 int currRow = _fieldTable.getSelectedRow();
366                                 closeTableComboBox(currRow);
367                                 _fieldTableModel.moveUp(currRow);
368                                 _fieldTable.setRowSelectionInterval(currRow-1, currRow-1);
369                         }
370                 });
371                 innerPanel3.add(_moveUpButton);
372                 _moveDownButton = new JButton(I18nManager.getText("button.movedown"));
373                 _moveDownButton.addActionListener(new ActionListener() {
374                         public void actionPerformed(ActionEvent e)
375                         {
376                                 int currRow = _fieldTable.getSelectedRow();
377                                 closeTableComboBox(currRow);
378                                 _fieldTableModel.moveDown(currRow);
379                                 _fieldTable.setRowSelectionInterval(currRow+1, currRow+1);
380                         }
381                 });
382                 innerPanel3.add(_moveDownButton);
383                 innerPanel3.add(Box.createVerticalStrut(60));
384                 JButton guessButton = new JButton(I18nManager.getText("button.guessfields"));
385                 guessButton.addActionListener(new ActionListener() {
386                         public void actionPerformed(ActionEvent e)
387                         {
388                                 _lastSelectedFields = null;
389                                 prepareSecondPanel();
390                         }
391                 });
392                 innerPanel3.add(guessButton);
393
394                 innerPanel2.add(innerPanel3, BorderLayout.EAST);
395                 secondCard.add(innerPanel2, BorderLayout.CENTER);
396                 JPanel altUnitsPanel = new JPanel();
397                 altUnitsPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
398                 altUnitsPanel.add(new JLabel(I18nManager.getText("dialog.openoptions.altitudeunits")));
399                 String[] units = {I18nManager.getText("units.metres"), I18nManager.getText("units.feet")};
400                 _unitsDropDown = new JComboBox(units);
401                 altUnitsPanel.add(_unitsDropDown);
402                 secondCard.add(altUnitsPanel, BorderLayout.SOUTH);
403                 _cardPanel.add(firstCard, "card1");
404                 _cardPanel.add(secondCard, "card2");
405
406                 wholePanel.add(_cardPanel, BorderLayout.CENTER);
407                 return wholePanel;
408         }
409
410
411         /**
412          * Close the combo box on the selected row of the field table
413          * @param inRow currently selected row number
414          */
415         private void closeTableComboBox(int inRow)
416         {
417                 TableCellEditor editor = _fieldTable.getCellEditor(inRow, 1);
418                 if (editor != null)
419                 {
420                         editor.stopCellEditing();
421                 }
422         }
423
424
425         /**
426          * change the status based on selection of a delimiter
427          */
428         protected void informDelimiterSelected()
429         {
430                 int fields = 0;
431                 // Loop through radios to see which one is selected
432                 for (int i=0; i<(_delimiterRadios.length-1); i++)
433                 {
434                         if (_delimiterRadios[i].isSelected())
435                         {
436                                 // Set label text to describe records and fields
437                                 int numRecords = _delimiterInfos[i].getNumRecords();
438                                 if (numRecords == 0)
439                                 {
440                                         _statusLabel.setText(I18nManager.getText("dialog.openoptions.deliminfo.norecords"));
441                                 }
442                                 else
443                                 {
444                                         fields = _delimiterInfos[i].getMaxFields();
445                                         _statusLabel.setText("" + numRecords + " " + I18nManager.getText("dialog.openoptions.deliminfo.records")
446                                                 + " " + fields + " " + I18nManager.getText("dialog.openoptions.deliminfo.fields"));
447                                 }
448                         }
449                 }
450                 // Don't show label if "other" delimiter is chosen (as records, fields are unknown)
451                 if (_delimiterRadios[_delimiterRadios.length-1].isSelected())
452                 {
453                         _statusLabel.setText("");
454                 }
455                 // enable/disable next button
456                 _nextButton.setEnabled((_delimiterRadios[4].isSelected() == false && fields > 1)
457                         || _otherDelimiterText.getText().length() == 1);
458         }
459
460
461         /**
462          * Get the delimiter info from the first step
463          * @return delimiter information object for the selected delimiter
464          */
465         public DelimiterInfo getSelectedDelimiterInfo()
466         {
467                 for (int i=0; i<4; i++)
468                         if (_delimiterRadios[i].isSelected()) return _delimiterInfos[i];
469                 // must be "other" - build info if necessary
470                 if (_delimiterInfos[4] == null)
471                         _delimiterInfos[4] = new DelimiterInfo(_otherDelimiterText.getText().charAt(0));
472                 return _delimiterInfos[4];
473         }
474
475
476         /**
477          * Use the delimiter selected to determine the fields in the file
478          * and prepare the second panel accordingly
479          */
480         private void prepareSecondPanel()
481         {
482                 DelimiterInfo info = getSelectedDelimiterInfo();
483                 FileSplitter splitter = new FileSplitter(_fileCacher);
484                 // Check info makes sense - num fields > 0, num records > 0
485                 // set "Finished" button to disabled if not ok
486                 // Add data to GUI elements
487                 String[][] tableData = splitter.splitFieldData(info.getDelimiter());
488                 // possible to ignore blank columns here
489                 _currentDelimiter = info.getDelimiter();
490                 _fileExtractTableModel.updateData(tableData);
491                 _fieldTableModel = new FieldSelectionTableModel();
492
493                 // Check number of fields and use last ones if count matches
494                 Field[] startFieldArray = null;
495                 if (_lastSelectedFields != null && splitter.getNumColumns() == _lastSelectedFields.length)
496                 {
497                         startFieldArray = _lastSelectedFields;
498                 }
499                 else
500                 {
501                         // Take first full row of file and use it to guess fields
502                         startFieldArray = FieldGuesser.guessFields(splitter.getFirstFullRow());
503                 }
504
505                 _fieldTableModel.updateData(startFieldArray);
506                 _fieldTable.setModel(_fieldTableModel);
507                 // add dropdowns to second column
508                 JComboBox fieldTypesBox = new JComboBox();
509                 String[] fieldNames = Field.getFieldNames();
510                 for (int i=0; i<fieldNames.length; i++)
511                 {
512                         fieldTypesBox.addItem(fieldNames[i]);
513                 }
514                 _fieldTable.getColumnModel().getColumn(1).setCellEditor(new DefaultCellEditor(fieldTypesBox));
515
516                 // Set altitude format to same as last time if available
517                 if (_lastAltitudeFormat == Altitude.Format.METRES)
518                         _unitsDropDown.setSelectedIndex(0);
519                 else if (_lastAltitudeFormat == Altitude.Format.FEET)
520                         _unitsDropDown.setSelectedIndex(1);
521                 // no selection on field list
522                 selectField(-1);
523         }
524
525
526         /**
527          * All options have been selected, so load file
528          */
529         private void finished()
530         {
531                 // Save delimiter, field array and altitude format for later use
532                 _lastUsedDelimiter = _currentDelimiter;
533                 _lastSelectedFields = _fieldTableModel.getFieldArray();
534                 Altitude.Format altitudeFormat = Altitude.Format.METRES;
535                 if (_unitsDropDown.getSelectedIndex() == 1)
536                 {
537                         altitudeFormat = Altitude.Format.FEET;
538                 }
539                 _lastAltitudeFormat = altitudeFormat;
540                 // give data to App
541                 SourceInfo sourceInfo = new SourceInfo(_file, SourceInfo.FILE_TYPE.TEXT);
542                 _app.informDataLoaded(_fieldTableModel.getFieldArray(),
543                         _fileExtractTableModel.getData(), altitudeFormat, sourceInfo);
544                 // clear up file cacher
545                 _fileCacher.clear();
546                 // dispose of dialog
547                 _dialog.dispose();
548         }
549
550
551         /**
552          * Make a panel with a label and a component
553          * @param inLabelKey label key to use
554          * @param inComponent component for main area of panel
555          * @return labelled Panel
556          */
557         private static JPanel makeLabelledPanel(String inLabelKey, JComponent inComponent)
558         {
559                 JPanel panel = new JPanel();
560                 panel.setLayout(new BorderLayout());
561                 panel.add(new JLabel(I18nManager.getText(inLabelKey)), BorderLayout.NORTH);
562                 panel.add(inComponent, BorderLayout.CENTER);
563                 return panel;
564         }
565
566
567         /**
568          * An entry in the field list has been selected
569          * @param inFieldNum index of field, starting with 0
570          */
571         private void selectField(int inFieldNum)
572         {
573                 if (inFieldNum == -1 || inFieldNum != _selectedField)
574                 {
575                         _selectedField = inFieldNum;
576                         _moveUpButton.setEnabled(inFieldNum > 0);
577                         _moveDownButton.setEnabled(inFieldNum >= 0
578                                 && inFieldNum < (_fieldTableModel.getRowCount()-1));
579                 }
580         }
581
582
583         /**
584          * @return the last delimiter character used for a load
585          */
586         public char getLastUsedDelimiter()
587         {
588                 return _lastUsedDelimiter;
589         }
590 }